User:Kephir/gadgets/rater/goldfish.js

/*

* Goldfish

* Copyright © 2013 Keφr at the English Wikipedia

*

* Goldfish is released under the terms of the GNU GPL version 2 or later,

* with the exception of a few specific functions released to the public domain.

* For the purpose of licensing, the various data used should not be considered

* a part of the script.

*

* "Goldfish" suggests this is something small, but this does not seem to be the case, actually.

* I should have probably called it "Moby Dick". After my own.

*/

//

mw.loader.using([

'mediawiki.util',

'mediawiki.api',

'mediawiki.Title',

/* XXX: drop these two dependencies */

'jquery.ui',

'jquery.ui'

], function () {

"use strict";

if (wgNamespaceNumber < 0)

return;

var GOLDFISH_VERSION = '2013-02-24';

var GOLDFISH_ADVERT = ' (with Goldfish)';

var settings = window.kephirGoldfish || {

promptToAssess: 0

// 0 = do not prompt, do not check even

// 1 = prompt to assess: highlight icon

// 2 = obnoxious prompt to assess: that, and display a popup message

};

importStylesheet('User:Kephir/gadgets/rater/goldfish.css');

// UI helper functions

//{ THESE FUNCTIONS ARE PUBLIC DOMAIN

var sh = {

el: function (tag, child, attr, events) {

var node = document.createElement(tag);

if (child) {

if ((typeof child === 'string') || (typeof child.length !== 'number'))

child = [child];

for (var i = 0; i < child.length; ++i) {

var ch = child[i];

if ((ch === void(null)) || (ch === null))

continue;

else if (typeof ch !== 'object')

ch = document.createTextNode(String(ch));

node.appendChild(ch);

}

}

if (attr) for (var key in attr) {

if ((attr[key] === void(0)) || (attr[key] === null))

continue;

node.setAttribute(key, String(attr[key]));

}

if (events) for (var key in events) {

node.addEventListener(key, events[key], false);

}

return node;

},

link: function (child, href, attr, ev) {

attr = attr || {};

ev = ev || {};

if (typeof attr === 'string') {

attr = { "title": attr };

}

if (typeof href === 'string')

attr.href = href;

else {

attr.href = 'javascript:void(null);';

ev.click = href;

}

return sh.el('a', child, attr, ev);

},

item: function (label, href, attr, ev, clbutt) {

return sh.el('li', [

sh.link(label, href, attr, ev)

], { "class": clbutt });

},

clear: function (node) {

while (node.hasChildNodes())

node.removeChild(node.firstChild);

}

};

// data grabber

var dataCache = { };

function grabData(kind) {

if (dataCache[kind] === void(null)) {

try {

$.ajax({ // XXX: jQuery sucks

'url': wgScript + '?action=raw&ctype=application/json&title=User:Kephir/gadgets/rater/' + kind + '.js',

'dataType': 'json',

'async': false, // fuck you, Douglas Crockford

'success': function (data) {

dataCache[kind] = data;

},

'error': function (xhr, message) {

throw new Error(message);

}

});

} catch (e) {

mw.util.jsMessage('Error retrieving "' + kind + '" data: ' + e.message + '. Goldfish will probably fail to work.');

dataCache[kind] = null;

}

}

return dataCache[kind];

}

//} END OF PUBLIC DOMAIN CODE

// completion helper

function attachCompletion(entry, callback) {

var tmout;

var uiCompleter = sh.el('ul', null, {

"class": "kephir-completion"

});

function generateList() {

var items = callback(entry.value);

while (uiCompleter.hasChildNodes())

uiCompleter.removeChild(uiCompleter.firstChild);

for (var i = 0; i < items.length; ++i) {

uiCompleter.appendChild(sh.el('li', [

sh.link(items[i].contents, function () {

items[i].callback.call(items[i]);

})

]));

}

}

uiCompleter.style.display = 'none';

uiCompleter.style.position = 'absolute';

uiCompleter.style.left = entry.offsetLeft + 'px';

uiCompleter.style.top = (entry.offsetTop + entry.offsetHeight) + 'px';

uiCompleter.style.minWidth = entry.offsetWidth + 'px';

entry.offsetParent.appendChild(uiCompleter);

entry.addEventListener('keypress', function () {

uiCompleter.style.display = 'none';

clearTimeout(tmout);

tmout = setTimeout(function () {

generateList();

uiCompleter.style.display = '';

}, 500);

}, false);

entry.addEventListener('blur', function () {

uiCompleter.style.display = 'none';

}, false);

}

// markup parser and editing model

MarkupError.prototype = Error.prototype;

function MarkupError(message, line) {

this.name = 'MarkupError';

this.message = message + ' at line ' + line;

this.line = line;

this.toString = function () {

return this.name + (this.message ? ': ' + this.message : '');

};

return this;

}

/*

* How markup parsing works

*

* The function below, parseMarkup() is a tokenizer: it takes raw markup

* and calls appropriate handlers when encountering meaningful fragments.

*

* The function below it, blobifyMarkup() calls parseMarkup() with handlers which

* build a markup block object containing blobs. A blob is an object which

* understands the structure of a specific markup fragment

* (a template invocation or a comment) and enables its easy manipulation.

* Markup block objects, besides containing blobs and simplifying serialisation

* (just call .toString()) maintain a list of marks - named pointers

* to specific locations within markup, easing template insertion.

*

* This is not a very good parser of MediaWiki markup. It is everything else

* which is good at avoiding feeding it with pathological input.

*/

function parseMarkup(code, handlers) {

var m, ms;

var stack = [];

var curline = 1;

function advance(n) {

curline += (code.substr(0, n).match(/\n/g) || []).length;

code = code.substr(n);

}

handlers.init(stack);

while (m = /^([^]*?)(\{\{\{?|}}}?|\||:|\[\[|]]|=|)*\s*[^\|<\[\]](?:[^\|<\[\]]+?|)*(?:\||]])/.test(code)) {

stack.unshift({

"mode": 'lpage'

});

handlers.linkStart(stack, m[2]);

} else {

handlers.text(stack, m[2]);

}

advance(m[2].length);

break;

case ']]':

switch (stack.length ? stack[0].mode : null) {

case 'lpage':

handlers.linkPage(stack, m[2]);

case 'ltext':

handlers.linkEnd(stack, m[2]);

stack.shift();

break;

default:

handlers.text(stack, m[2]);

}

advance(m[2].length);

break;

case '{{{':

if (/^\{\{\{(?!\{)/.test(code)) {

stack.unshift({

'mode': 'pname',

'name': ''

});

handlers.paramRefStart(stack);

advance(m[2].length);

break;

}

m[2] = '{{';

/* fallthrough */

case '{{':

if (/^\{\{(?:\s*)*\s*#(?:[^<\|:]+?|)*:/.test(code)) {

switch (stack.length ? stack[0].mode : null) {

case 'pname':

case 'fname':

case 'tname':

throw new MarkupError('Cannot accept a ParserFunction inside a ' + stack[0].mode);

default:

}

stack.unshift({

'mode': 'fname'

});

handlers.funcStart(stack);

advance(m[2].length);

} else if (/^\{\{(?:\s*)*\s*[^\s\|<](?:[^\|<]+?|)*(?:\||}})/.test(code)) {

switch (stack.length ? stack[0].mode : null) {

case 'pname':

case 'fname':

case 'tname':

throw new MarkupError('Cannot accept a template inside a ' + stack[0].mode);

default:

}

stack.unshift({

'mode': 'tname',

'name': '',

'line': curline

});

handlers.templateStart(stack);

advance(/^\{\{\s*/.exec(code)[0].length);

} else {

handlers.text(stack, m[2]);

advance(m[2].length);

}

break;

case '}}}':

if (stack.length && ((stack[0].mode === 'pname') || (stack[0].mode === 'pvalue') || (stack[0].mode === 'pignore'))) {

switch (stack[0].mode) {

case 'pname':

handlers.paramRefName(stack);

break;

case 'pvalue':

handlers.paramRefDefault(stack);

break;

case 'pignore':

handlers.paramRefPipe(stack);

break;

}

handlers.paramRefEnd(stack);

advance(m[2].length);

stack.shift();

break;

}

m[2] = '}}';

/* fallthrough */

case '}}':

switch (stack.length ? stack[0].mode : null) {

case 'tname':

stack[0].name = stack[0].name.replace(/\s*$/, "");

handlers.templateName(stack);

handlers.templateEnd(stack);

stack.shift();

break;

case 'tkey':

case 'tvalue':

handlers.templateParam(stack);

handlers.templateEnd(stack);

stack.shift();

break;

case 'fname':

handlers.funcName(stack);

handlers.funcEnd(stack);

stack.shift();

break;

case 'fparam':

handlers.funcParam(stack);

handlers.funcEnd(stack);

stack.shift();

break;

default:

handlers.text(stack, m[2]);

}

advance(m[2].length);

break;

case '|':

switch (stack.length ? stack[0].mode : null) {

case 'tname':

stack[0].name = stack[0].name.replace(/\s*$/, "");

handlers.templateName(stack);

stack[0].mode = 'tkey';

break;

case 'tkey':

case 'tvalue':

handlers.templateParam(stack);

stack[0].mode = 'tkey';

break;

case 'pname':

handlers.paramRefName(stack);

stack[0].mode = 'pvalue';

break;

case 'pvalue':

handlers.paramRefDefault(stack);

stack[0].mode = 'pignore';

break;

case 'pignore':

handlers.paramRefPipe(stack);

break;

case 'lpage':

stack[0].mode = 'ltext';

handlers.linkPage(stack, m[2]);

break;

case 'fparam':

handlers.funcParam(stack);

stack[0].fpnum++;

break;

default:

handlers.text(stack, m[2]);

}

advance(m[2].length);

break;

case ':':

switch (stack.length ? stack[0].mode : null) {

case 'fname':

handlers.funcName(stack);

stack[0].mode = 'fparam';

stack[0].fpnum = 0;

break;

default:

handlers.text(stack, m[2].length);

}

advance(m[2].length);

break;

case '=':

switch (stack.length ? stack[0].mode : null) {

case 'tkey':

handlers.templateEqual(stack);

stack[0].mode = 'tvalue';

break;

default:

handlers.text(stack, m[2]);

}

advance(m[2].length);

break;

case '

case '

case '

if (ms = /^<(nowiki|pre|includeonly)(?:\s+([a-z]=".*?"\s*)*)?>([^]*?)<\/\1>/.exec(code)) {

handlers.noWiki(stack, ms[0], ms[3]);

advance(ms[0].length);

} else {

throw new MarkupError("Broken or

 tag", curline);

}

break;

case '/.exec(code)) {

handlers.comment(stack, ms[0], ms[1]);

advance(ms[0].length);

} else

throw new MarkupError("Broken comment", curline);

break;

default:

throw new MarkupError('"Should not happen" error - got "' + m[2] + '" from parser', curline);

}

}

handlers.text(stack, code);

handlers.end(stack);

if (stack.length !== 0) {

throw new Error("Broken invocation for {{" + stack[0].name + "}} (started at line " + stack[0].line + ") at line " + curline);

}

}

function CommentBlob(content) {

this.getContent = function () {

return content;

}

this.setContent = function (newContent) {

return content = newContent;

};

this.toString = function (plain) {

if (plain)

return '';

else

return '';

}

}

function MarkupBlock() {

var contents = [];

var marks = {};

this.push = function () {

contents.push.apply(contents, arguments);

this.length = contents.length;

};

this.hasMark = function (name) {

return name in marks;

}

this.setMark = function (name) {

marks[name] = contents.length;

};

this.item = function (i) {

return contents[i];

};

this.removeMark = function (name) {

if (typeof marks[name] !== 'number') {

return this.iterateBlocks(function (block) {

if (block === this)

return false;

if (block.removeMark(name))

return true;

});

}

delete marks[name];

return true;

};

this.remove = function (index, count) {

if ((count === void(0)) || (count === null))

count = 1;

for (var key in marks) {

if (typeof marks[key] === 'number')

if (marks[key] > index)

marks[key] -= count;

}

contents = contents.slice(0, index).concat(contents.slice(index + count));

this.length = contents.length;

};

this.insertBefore = function (item, mark) {

if (typeof marks[mark] !== 'number') {

if (marks[mark]) {

return marks[mark].insertBefore(item, mark);

}

return this.iterateBlocks(function (block) {

if (block === this)

return false;

if (block.insertBefore(item, mark)) {

marks[mark] = block;

return true;

}

});

}

contents.splice(marks[mark], 0, item);

for (var key in marks) {

if (typeof marks[key] === 'number')

if (marks[key] >= marks[mark])

marks[key]++;

}

return true;

};

this.trim = function () {

var adjust = 0;

while ((typeof contents[0] === 'string') && /^\s*$/.test(contents[0])) {

contents[i].shift();

adjust++;

}

while ((typeof contents[contents.length - 1] === 'string') && /^\s*$/.test(contents[contents.length - 1])) {

contents[i].pop();

adjust++;

}

if (typeof contents[0] === 'string')

contents[0] = contents[0].replace(/^\s*/, '');

if (typeof contents[contents.length - 1] === 'string')

contents[contents.length - 1] = contents[contents.length - 1].replace(/\s*$/, '');

for (var key in marks) {

if (typeof marks[key] === 'number')

if (marks[key] >= marks[mark])

if ((marks[key] -= adjust) > contents.length) {

marks[key] = contents.length;

}

}

};

this.clone = function () {

var that = new MarkupBlock();

for (var i = 0; i < contents.length; ++i) {

that.push(contents[i]);

}

return that;

};

this.iterateBlocks = function (iterator, andSelf) {

if (andSelf && iterator(this))

return true;

for (var i = 0; i < contents.length; ++i) {

if (contents[i].iterateBlocks)

if (contents[i].iterateBlocks(iterator, true))

return true;

}

};

this.iterateBlobs = function (iterator) {

for (var i = 0; i < contents.length; ++i) {

if (iterator(contents[i], i))

return true;

if (contents[i].iterateBlobs)

if (contents[i].iterateBlobs(iterator))

return true;

}

};

this.toString = function (plain) {

if (plain) {

var r = '';

for (var i = 0; i < contents.length; ++i) {

if ((typeof contents[i] !== 'number') && contents[i].toString)

r += contents[i].toString(true);

else

r += String(contents[i]);

}

return r.replace(/^\s+|\s+$/g, "");

} else

return contents.join("");

};

this.push.apply(this, arguments);

}

MarkupBlock.fromString = function () {

var block = new MarkupBlock();

block.push.apply(block, arguments);

return block;

};

function TemplateBlob(nameBlock) {

var paramBlocks = [];

var associations = {};

this.hasKey = function (key) {

return key in associations;

}

this.setNameBlock = function (block) {

nameBlock = block;

};

this.getName = function () {

var t = new mw.Title(nameBlock.toString(true));

if (t.ns === 0) {

if (!/^:/.test(t.name)){

t.ns = 10;

}

}

return t.toText();

};

this.getPlainValue = function (key) {

key = String(key);

return key in associations ? associations[key].value.toString(true) : null;

}

this.setPlainValue = function (key, value) {

key = String(key);

if (key in associations) {

// XXX: try to preserve whitespace

associations[key].value = MarkupBlock.fromString(value);

} else {

this.associate(MarkupBlock.fromString(key), MarkupBlock.fromString(value));

}

};

this.associate = function (key, value) {

paramBlocks.push(associations[(typeof key === 'number') ? String(key) : key.toString(true)] = {

"key": key,

"value": value

});

};

this.iterateBlocks = function (iterator) {

if (nameBlock.iterateBlocks(iterator, true))

return true;

for (var i = 0; i < paramBlocks.length; ++i) {

if (paramBlocks[i].key.iterateBlocks)

if (paramBlocks[i].key.iterateBlocks(iterator, true))

return true;

if (paramBlocks[i].value.iterateBlocks(iterator, true))

return true;

}

}

this.iterateBlobs = function (iterator) {

if (nameBlock.iterateBlobs(iterator))

return true;

for (var i = 0; i < paramBlocks.length; ++i) {

if (paramBlocks[i].key.iterateBlobs)

if (paramBlocks[i].key.iterateBlobs(iterator))

return true;

if (paramBlocks[i].value.iterateBlobs(iterator))

return true;

}

}

this.toString = function (plain) {

var r = '{{' + nameBlock.toString(plain);

for (var i = 0; i < paramBlocks.length; ++i) {

r += '|' + (typeof paramBlocks[i].key !== 'number' ? paramBlocks[i].key.toString(plain) + '=' : '') + paramBlocks[i].value.toString(plain);

}

return r + '}}';

};

}

function ParamRefBlob(nameBlock) {

this.getName = function () {

return nameBlock.toString(true);

};

}

function blobifyMarkup(code, handlers) {

var block = new MarkupBlock();

var curblock = block;

parseMarkup(code, {

init: function (stack) {

block.setMark("last-template");

if (handlers.init)

handlers.init(stack, curblock, block);

},

text: function (stack, raw) {

curblock.push(raw);

},

comment: function (stack, raw, content) {

curblock.push(new CommentBlob(content));

},

templateStart: function (stack) {

stack[0].curValue = new MarkupBlock();

curblock.push(stack[0].blob = new TemplateBlob(stack[0].curValue));

curblock = stack[0].curValue;

},

templateName: function (stack) {

if (handlers.templateName)

handlers.templateName(stack, curblock, block);

stack[0].curKey = stack[0].curPos = 1;

curblock = stack[0].curValue = new MarkupBlock();

},

templateParam: function (stack) {

if (handlers.templateParam)

handlers.templateParam(stack, curblock, block);

stack[0].blob.associate(stack[0].curKey, stack[0].curValue);

stack[0].curKey = ++stack[0].curPos;

curblock = stack[0].curValue = new MarkupBlock();

},

templateEqual: function (stack) {

stack[0].curKey = stack[0].curValue;

stack[0].curPos--;

curblock = stack[0].curValue = new MarkupBlock();

},

templateEnd: function (stack) {

if (handlers.templateEnd)

handlers.templateEnd(stack, curblock, block);

curblock = stack[1] ? stack[1].curValue : block;

if (!stack[1]) {

block.setMark("last-template");

}

},

paramRefStart: function (stack) {

stack[0].curValue = curblock = new MarkupBlock();

curblock.push(stack[0].blob = new ParamRefBlob(curblock));

},

paramRefName: function (stack) {

if (handlers.paramRefName)

handlers.paramRefName(stack, curblock, block);

curblock = stack[0].curValue = new MarkupBlock();

},

paramRefDefault: function (stack) {

if (handlers.paramRefDefault)

handlers.paramRefDefault(stack, curblock, block);

stack[0].blob.setDefault(curblock);

curblock = stack[0].curValue = new MarkupBlock();

},

paramRefPipe: function (stack) {

stack[0].blob.addExtra(curblock);

curblock = stack[0].curValue = new MarkupBlock();

},

paramRefEnd: function (stack) {

curblock = stack[1] ? stack[1].curValue : block;

},

linkStart: function (stack, raw) {

stack[0].curValue = curblock;

curblock.push(raw);

},

linkPage: function (stack, raw) {

curblock.push(raw);

},

linkEnd: function (stack, raw) {

curblock.push(raw);

},

funcStart: function (stack) {

stack[0].curValue = curblock = new MarkupBlock();

curblock.push(stack[0].blob = new ParserFunctionBlob(curblock));

},

funcName: function (stack) {

stack[0].curValue = curblock = new MarkupBlock();

},

funcParam: function (stack) {

stack[0].blob.pushArg(curblock);

},

funcEnd: function (stack) {

curblock = stack[1] ? stack[1].curValue : block;

},

noWiki: function (stack, raw, contents) {

curblock.push(raw);

},

end: function (stack) {

if (handlers.end)

handlers.end(stack, curblock, block);

}

});

return block;

}

// editing modules

var editModules = { };

(function () { // zoo - talk page notices

// Do not feed the animals

editModules.zoo = {

editor: {

init: function (state, ui) {

},

templateName: function (state, ui) {

},

templateParam: function (state, ui) {

},

templateEnd: function (state, ui) {

},

end: function (state, ui) {

}

},

checker: {

init: function (state, ui) {

},

templateName: function (state, ui) {

},

templateParam: function (state, ui) {

},

templateEnd: function (state, ui) {

},

end: function (state, ui) {

}

}

}

})();

(function () { // aquarium - Article Quality Rating Metric

function createTemplateUI(templ, state, ui) {

var uiRating, lastRated = null;

var sumdata = { };

function computeRating(scores) {

if (scores.compr === null)

return null;

if (scores.compr < 3)

return 'Stub';

if ((scores.compr >= 7) && (scores.sourc >= 4) && (scores.reada >= 2) && (scores.neutr >= 2))

return 'B';

if ((scores.compr >= 4) && (scores.sourc >= 2))

return 'C';

return 'Start';

}

function updateScore() {

uiRating.data = computeRating({

compr: templ.getPlainValue("comprehensiveness"),

sourc: templ.getPlainValue("sourcing"),

reada: templ.getPlainValue("readability"),

neutr: templ.getPlainValue("neutrality")

}) || 'none';

state.aquarium.uiSection.setSummary('rating: ' + uiRating.data);

}

function createScoreControl(parm, desc, min, max) {

var sel;

var item = sh.el('li', [

sh.el('label', [desc, sel = sh.el('select', [

sh.el('option', '?', { "value": "" })

], null, {

"change": function () {

templ.setPlainValue("rater", '{{subst' + ':REVISIONUSER}}');

templ.setPlainValue("time", '~~' + '~' + '~~');

templ.setPlainValue("oldid", state.page.getLastRevision());

templ.setPlainValue(parm, this.value == '' ? null : this.value);

sumdata[parm] = this.value;

updateScore();

ui.makeEditorDirty();

ui.refreshSummary();

}

})])

]);

for (var i = min; i <= max; ++i) {

sel.appendChild(sh.el('option', String(i), { "value": String(i) }));

}

sel.value = parseInt(templ.getPlainValue(parm), 10);

return item;

}

ui.addSummaryHook(function () {

var r = [];

if ('comprehensiveness' in sumdata) {

r[r.length] = 'Comp=' + sumdata.comprehensiveness;

}

if ('sourcing' in sumdata) {

r[r.length] = 'Src=' + sumdata.sourcing;

}

if ('neutrality' in sumdata) {

r[r.length] = 'Neut=' + sumdata.neutrality;

}

if ('readability' in sumdata) {

r[r.length] = 'Read=' + sumdata.readability;

}

if ('formatting' in sumdata) {

r[r.length] = 'Fmt=' + sumdata.formatting;

}

if ('illustrations' in sumdata) {

r[r.length] = 'Illu=' + sumdata.illustrations;

}

if (r.length) {

var rating = computeRating({

compr: templ.getPlainValue("comprehensiveness"),

sourc: templ.getPlainValue("sourcing"),

reada: templ.getPlainValue("readability"),

neutr: templ.getPlainValue("neutrality")

});

return 'AQRM: ' + r.join(", ") + (rating ? ' (' + rating + '-class)' : '');

} else

return;

});

lastRated = {};

if (templ.hasKey("rater") && templ.hasKey("oldid") && templ.hasKey("time")) {

lastRated.user = templ.getPlainValue("rater");

lastRated.oldid = templ.getPlainValue("oldid");

lastRated.time = templ.getPlainValue("time");

if ((lastRated.time === ('~~' + '~' + '~~')) || (lastRated.user === ('{{subst' + ':REVISIONUSER}}'))) {

lastRated = null;

}

} else {

lastRated = null;

}

var uiTempl = sh.el('div', [

sh.el('ul', [

createScoreControl("comprehensiveness", "Comprehensiveness", 1, 10),

createScoreControl("sourcing" , "Sourcing" , 0, 6),

createScoreControl("neutrality" , "Neutrality" , 0, 3),

createScoreControl("readability" , "Readability" , 0, 3),

createScoreControl("formatting" , "Formatting" , 0, 2),

createScoreControl("illustrations" , "Illustrations" , 0, 2),

], { "class": "aqrm-scores" }),

sh.el('p', ['Computed rating: ', sh.el('strong', [uiRating = document.createTextNode('none')])]),

lastRated ? sh.el('p', [

'This page was last rated by ',

sh.link(lastRated.user, mw.util.getUrl('User:' + lastRated.user)),

' on ',

sh.el('strong', lastRated.time),

' at revision ',

sh.link(String(lastRated.oldid), wgScript + '?oldid=' + lastRated.oldid)

]) : void(0)

]);

updateScore();

return uiTempl;

}

editModules.aquarium = {

editor: {

init: function (state, ui, block) {

state.aquarium = {};

state.aquarium.uiSection = ui.addEditorSection([

sh.link('Article quality rating metric',

mw.util.getUrl('Wikipedia:Ambassadors/Research/Article quality')

)

], 'aqrm');

state.aquarium.uiSection.setSummary('no template present');

state.aquarium.uiSection.body.appendChild(

state.aquarium.uiMsg = sh.el('p', [

'No scoring template present. ',

sh.link('Add template', function () {

state.aquarium.templ = new TemplateBlob(MarkupBlock.fromString("Quality assessment"));

block.insertBefore(state.aquarium.templ, "last-template"); // XXX

sh.clear(state.aquarium.uiMsg);

state.aquarium.uiSection.setSummary('');

state.aquarium.uiSection.body.appendChild(

createTemplateUI(state.aquarium.templ, state, ui)

);

ui.makeEditorDirty();

})

])

);

},

templateName: function (state, ui, stack, block) {

},

templateParam: function (state, ui) {

},

templateEnd: function (state, ui, topblock, curblock, stack) {

if (stack[0].blob.getName() === 'Template:Quality assessment') {

sh.clear(state.aquarium.uiMsg)

state.aquarium.uiSection.setSummary('');

if (state.aquarium.templ) {

state.aquarium.uiMsg.appendChild(sh.el('span', [

sh.el('strong', 'Warning'), ': ',

'There is more than one assessment template. Will only take care of the last one.'

]));

}

state.aquarium.uiSection.body.appendChild(

createTemplateUI(state.aquarium.templ = stack[0].blob, state, ui)

);

}

},

end: function (state, ui) {

}

}

};

})();

(function () { // jungle - Wikipedia 1.0 Assessment

var projList = grabData('project-list');

for (var key in projList) {

if (!projList[key])

continue;

var aliases = projList[key].aliases;

for (var i = 0; i < aliases.length; ++i) {

projList[aliases[i]] = projList[key];

}

}

var projData = { };

var projDataSrc = { };

var bannerShells = [

"Template:WikiProjectBannerShell",

// {{WikiProjectBannerShell}}

"Template:Shell",

"Template:WBPS",

"Template:WikiProject",

"Template:WikiProject",

"Template:Wikiprojectbannershell",

"Template:WPBannerShell",

"Template:Wpbs",

"Template:WPBS",

// {{WikiProject Banners}}

"Template:WikiProject Banners",

"Template:WPB",

"Template:Wpb",

"Template:Wikiprojectbanners"

];

var bannerMeta = [

"Template:Metabanner",

"Template:WikiProject Notice",

"Template:WikiProjectBannerMeta",

"Template:WikiProjectNotice",

"Template:WPBM",

"Template:WPStructure",

"Wikipedia:Wpbm",

"Wikipedia:WPBM"

];

function generateData(source) {

var legit = false;

var params = {

};

var data = {

stdParams: {}

};

var buffer;

blobifyMarkup(source, {

init: function (stack) {

// XXX

},

templateName: function (stack, curblock, block) {

if (bannerMeta.indexOf(stack[0].blob.getName()) !== -1) {

legit = true;

}

},

templateParam: function (stack, curblock, block) {

var pname = typeof stack[0].curKey === 'number' ? stack[0].curKey : stack[0].curKey.toString(true);

var pvalue = stack[0].curValue;

var pparam;

pvalue.trim();

switch (tname) {

case 'Template:WPBannerMeta':

switch (pname) {

case 'small':

case 'auto':

case 'class':

case 'importance':

case 'priority':

case 'listas':

case 'attention':

case 'infobox':

pparam = pvalue.item(0);

if (pparam instanceof ParamRefBlob) {

data.stdParams[pname] = pparam.getName();

}

encounters[pparam.getName()] = true;

break;

case 'PROJECT': // the name of the project

data.project = 'WikiProject ' + pvalue.toString(true);

break;

case 'PROJECT_NAME': // project name (if it does not start with "WikiProject ")

data.project = pvalue.toString(true);

break;

case 'QUALITY_SCALE':

data.qualityScale = pvalue.toString(true); // standard/extended/inline/subpage

break;

case 'IMPORTANCE_SCALE':

data.importanceScale = pvalue.toString(true); // standard/inline/subpage

break;

}

break;

case 'Template:WPBannerMeta/hooks/notes':

break;

case 'Template:WPBannerMeta/hooks/bchecklist':

break;

case 'Template:WPBannerMeta/hooks/collaboration':

break;

case 'Template:WPBannerMeta/hooks/taskforces':

break;

}

},

templateEnd: function (stack, curblock, block) {

var name = stack[0].blob.getName();

// commit data to

},

paramRefName: function (stack, curblock, block) {

var pname = curblock.toString(true);

if (!(pname in encounters))

encounters[pname] = false;

},

end: function (curblock, block) {

}

});

if (!legit)

return null;

return data;

}

function grabProjectData(name, handlers) {

if (name in projData) {

if (projData[name] === null) {

handlers.nak();

} else {

handlers.ack(projData[name], projDataSrc[name]);

}

return;

}

// XXX: no data yet - download it

$.ajax({ // XXX: jQuery sucks

'url': wgScript + '?action=raw&ctype=application/json&title=' + name + '/rater.json',

'dataType': 'json'

}).done(function (result) {

handlers.ack(

projData[name] = result,

projDataSrc[name] = 'json'

);

}).fail(function () {

// XXX: check what error happened first

$.ajax({ // XXX: jQuery sucks

'url': wgScript + '?action=raw&ctype=application/json&title=' + name,

'dataType': 'text'

}).done(function (result) {

var data;

try {

data = projData[name] = generateData(result);

} catch (e) {

handlers.error(e); // XXX

return;

}

if (data === null)

if (name in projList) {

handlers.error(); // XXX

} else {

handlers.nak();

}

else

handlers.ack(

data,

projDataSrc[name] = 'generated'

);

}).fail(function () { // jQuery sucks even more than I thought

// XXX: error details

handlers.error();

});

});

// 3. if a positive entry, download the template's data

// 1. if no data available, download its source and autogenerate data

// 4. if no, download its source and check if it calls {{WPBannerMeta}}

// 1. if it does not, console.info("suggest negative entry for {{xxx}}") and pass

// 2. if it does, autogenerate data

}

// a jungle of templates, that is what WP:1.0 is.

// some hints, so you will not get apeshit:

// ayeaye - assessment

// capuchin - checklists

// rhesus - requests

// tarsier - task forces

// orangutan - other

var monkey = {

};

// each template is scanned, and the monkeys generate appropriate interface for parameters encountered.

editModules.jungle = {

editor: {

init: function (state, ui) {

var uiNewBannerInput;

state.jungle = {};

state.jungle.shell = null;

state.jungle.section = ui.addEditorSection([

sh.link('Version 1.0 Editorial Team assessment scheme',

mw.util.getUrl('Wikipedia:Version 1.0 Editorial Team/Assessment')

)

], 'jungle');

state.jungle.section.body.appendChild(

state.jungle.uiList = sh.el('ul', [

// empty

])

);

state.jungle.section.body.appendChild(

sh.el('form', [

uiNewBannerInput = sh.el('input', null, {

"class": "name",

"size": "48",

"placeholder": "new banner"

}),

sh.el('input', null, {

"type": "submit",

"value": "add"

})

], {

"action": "javascript:void(0);",

"class": "new-banner"

}, {

"submit": function (ev) {

ev.preventDefault();

// XXX:

// 1. if there is a shell, add template at the end of the shell

// 2. if there is no shell and there are two templates already, wrap them into a shell and put the new template into it

// 3. otherwise put after the last template

uiNewBannerInput.value = '';

}

})

);

},

templateName: function (state, ui, topblock, curblock, stack) {

var name = stack[0].blob.getName();

if (bannerShells.indexOf(name) !== -1) {

state.jungle.shell = stack[0].blob;

// XXX: how to handle multiple banner shells?

}

},

templateParam: function (state, ui) {

},

templateEnd: function (state, ui, topblock, curblock, stack) {

var blob = stack[0].blob;

var name = blob.getName();

var uiItem, uiWait, uiName;

if (name in projList)

if (projList[name] === null)

return;

state.jungle.uiList.appendChild(uiItem = sh.el('li', [

sh.el('span', [

sh.link(

[uiName = document.createTextNode(

(projList[name] ? projList[name].project : null) || name.replace(/^Template:(WikiProject ?|WP ?)?/, '')

)],

mw.util.getUrl(name)

),

': '

], { "class": "" }),

uiWait = sh.el('span', ['loading data...'], { "class": "wait" })

]));

grabProjectData(name, {

ack: function (data) {

var paramsHandled = {};

uiWait.parentNode.removeChild(uiWait);

uiItem.appendChild(sh.el(

));

blob.enumParams(function (key) {

paramsHandled[key] = false;

});

if (data.project)

uiName.project = data.project;

for (var key in monkey) {

monkey[key](state, paramsHandled, data, uiItem);

}

for (var key in paramsHandled) {

if (!paramsHandled[key]) {

// XXX

}

}

},

nak: function () {

console.info('suggesting negative entry for: ' + name);

uiItem.parentNode.removeChild(uiItem);

},

error: function (err) {

console.info(err);

uiWait.parentNode.removeChild(uiWait);

uiItem.appendChild(sh.el('span', [

'error'

]));

}

});

},

end: function (state, ui) {

}

},

checker: {

init: function (state, ui) {

},

templateName: function (state, ui) {

},

templateParam: function (state, ui) {

},

templateEnd: function (state, ui) {

},

end: function (state, ui) {

}

}

};

})();

// general backend code

var api = new mw.Api();

var talkPageHeader = null;

function Page(mdata) {

this.grabTalkHeader = function (handlers, force) {

if (!force && talkPageHeader) {

handlers.ok(talkPageHeader);

return true;

}

api.get({

action: 'query',

prop: 'info|revisions',

rvprop: 'timestamp|content',

rvsection: 0,

rvlimit: 1,

rvdir: 'older',

intoken: 'edit',

titles: mdata.talkPageName

}).done(function (result) {

var tpgpid = Object.keys(result.query.pages)[0];

var tpg = result.query.pages[tpgpid];

handlers.ok(talkPageHeader = result.query.pages[tpgpid]);

}).fail(handlers.error);

};

this.saveTalkHeader = function (markup, summary, handlers) {

api.post({

action: 'edit',

section: 0,

title: mdata.talkPageName,

basetimestamp: talkPageHeader.revisions ? talkPageHeader.revisions[0].timestamp : void(0),

starttimestamp: talkPageHeader.starttimestamp,

token: talkPageHeader.edittoken,

notminor: true,

summary: summary,

watchlist: 'nochange',

text: markup

}).done(function (result) {

talkPageHeader = null;

handlers.ok(result);

}).fail(handlers.error);

};

this.getTalkPageName = function () {

return mdata.talkPageName;

}

this.getLastRevision = function () {

if (typeof mdata.lastRevision !== 'number') {

api.get({

action: 'query',

prop: 'ids|revisions',

rvprop: 'ids|timestamp',

rvsection: 0,

rvlimit: 1,

rvdir: 'older',

titles: mdata.pageName,

async: false

}).done(function (result) {

var pageid = Object.keys(result.query.pages)[0];

mdata.lastRevision = result.query.pages[pageid].revisions[0].revid;

})

}

return mdata.lastRevision;

};

}

// general UI code

function UserInterface(page) {

var self = this;

var uiEditorTab, uiSourceTab, uiPreviewTab, uiCurrentTab, uiStatusBar;

var uiSourceBox, uiSourceErr, uiPreviewBin, uiEditor, uiSummary;

var dirtySource = false, dirtyEditor = false, dirtySummary = false;

var block;

var uiTabLabel = [], uiTab = [];

var uiActiveTabLabel, uiActiveTab;

var talkPageData;

var summaryHooks = [];

function setActiveTab(i) {

if (uiActiveTabLabel)

uiActiveTabLabel.classList.remove("active");

if (uiActiveTab)

uiActiveTab.style.display = 'none';

uiTabLabel[i].classList.add("active");

uiTab[i].style.display = 'block';

uiActiveTab = uiTab[i];

uiActiveTabLabel = uiTabLabel[i];

}

function bailOutParsing(message) {

sh.clear(uiSourceErr);

uiSourceErr.appendChild(document.createTextNode(message)); // XXX: prettier?

dirtyEditor = false;

dirtySource = true;

setActiveTab(1);

}

this.makeEditorDirty = function () {

dirtyEditor = true;

};

this.clearEditor = function () {

block = null;

summaryHooks = [];

sh.clear(uiEditor);

};

this.setStatusBar = function (msg) {

sh.clear(uiStatusBar);

uiStatusBar.appendChild(sh.el('span', msg));

}

this.addEditorSection = function (title, clbutt) {

var summaryNode;

var summarySpan;

var hidden = false;

var sectBody = sh.el('dd', null, { "class": clbutt });

var sectHead = sh.el('dt', [

sh.el('span', ['[',

sh.link('hide', function () {

hidden = !hidden;

sectBody.style.display = hidden ? 'none' : '';

summarySpan.style.display = hidden ? '' : 'none';

this.firstChild.data = hidden ? 'show' : 'hide';

}),

']'], { "class": "hide-link" }),

sh.el('span', title, { "class": "title" }),

summarySpan = sh.el('span', [summaryNode = document.createTextNode('')]),

]);

summarySpan.style.display = 'none';

uiEditor.appendChild(sectHead);

uiEditor.appendChild(sectBody);

return {

"head": sectHead,

"body": sectBody,

setSummary: function (text) {

summaryNode.data = (text && ': ') + text;

}

};

}

this.refreshSummary = function () {

if (dirtySummary)

return;

var sum = summaryHooks.map(function (hook) {

return hook();

}).filter(function (item) {

return item !== void(0);

});

uiSummary.value = sum.length ? sum.join("; ") + GOLDFISH_ADVERT : '';

};

this.addSummaryHook = function (hook) {

summaryHooks.push(hook);

};

this.prepareEditor = function (source) {

var state = {

"page": page

};

this.clearEditor();

return block = blobifyMarkup(source, {

init: function (curblock, topblock) {

for (var key in editModules) {

if (editModules[key].editor && editModules[key].editor.init) {

editModules[key].editor.init(state, self, topblock);

}

}

},

templateName: function (stack, curblock, topblock) {

for (var key in editModules) {

if (editModules[key].editor && editModules[key].editor.templateName) {

editModules[key].editor.templateName(state, self, topblock, curblock, stack);

}

}

},

templateParam: function (stack, curblock, topblock) {

for (var key in editModules) {

if (editModules[key].editor && editModules[key].editor.templateParam) {

editModules[key].editor.templateParam(state, self, topblock, curblock, stack);

}

}

},

templateEnd: function (stack, curblock, topblock) {

for (var key in editModules) {

if (editModules[key].editor && editModules[key].editor.templateEnd) {

editModules[key].editor.templateEnd(state, self, topblock, curblock, stack);

}

}

},

end: function (curblock, topblock) {

for (var key in editModules) {

if (editModules[key].editor && editModules[key].editor.end) {

editModules[key].editor.end(state, self, topblock);

}

}

self.refreshSummary();

}

});

};

this.prepare = function (talkHeader, keepTab) {

var source = talkHeader.revisions ? talkHeader.revisions[0]['*'] : '';

uiSourceBox.value = source;

dirtySummary = false;

uiSummary.value = '';

try {

this.prepareEditor(source);

} catch (e) {

bailOutParsing(e.message);

debugger;

return;

}

dirtySource = false;

dirtyEditor = false;

if (!keepTab) {

setActiveTab(0);

}

};

var uiTitle, uiContent, uiFooter;

var uiBox = this.box = sh.el('div', [

uiTitle = sh.el('div', [

sh.el('span', [

sh.el('strong', "Goldfish"),

" version ",

GOLDFISH_VERSION,

" by ",

sh.link("Keφr", mw.util.getUrl('User:Kephir'))

]),

sh.el('ul', [

sh.item("Feedback",

wgScript + '?title=' + mw.util.wikiUrlencode('User talk:Kephir/gadgets/rater') +

'&action=edit§ion=new&preloadtitle=Feedback&editintro=' +

mw.util.wikiUrlencode('User:Kephir/gadgets/rater/feedback-editintro')

),

sh.item("×", function (ev) {

ev.preventDefault();

self.show(false);

}, "Close")

], { "class": "link-list buttons" }),

sh.el('br')

], { "class": "title" }),

sh.el('ul', [

sh.item("Reload", function (ev) {

page.grabTalkHeader({

ok: function (talkHeader) {

self.prepare(talkHeader, true);

},

error: function () {

mw.util.jsMessage('Error grabbing talk page revisions. See console for details.');

console.error(arguments);

}

}, true);

}, null, null, "item-reload"),

uiTabLabel[0] = sh.item("Editor", function (ev) {

if (dirtySource) {

try {

self.prepareEditor(uiSourceBox.value);

} catch (e) {

bailOutParsing(e.message);

debugger;

return;

}

dirtySource = false;

}

setActiveTab(0);

}),

uiTabLabel[1] = sh.item("Source", function (ev) {

sh.clear(uiSourceErr);

if (dirtyEditor) {

uiSourceBox.value = block.toString();

dirtyEditor = false;

}

setActiveTab(1);

}),

uiTabLabel[2] = sh.item("Preview", function (ev) {

if (dirtyEditor) {

uiSourceBox.value = block.toString();

dirtyEditor = false;

}

var source = uiSourceBox.value;

api.post({

'action': 'parse',

'title': page.getTalkPageName(),

'text': source,

'pst': '1',

'prop': 'text',

'disablepp': 1

}).done(function (result) {

uiPreviewBin.innerHTML = result.parse.text['*'];

setActiveTab(2);

}).fail(function () {

// XXX: show error message besides uiPreviewBin

console.error(arguments);

});

})

], { "class": "link-list tabs" }),

uiContent = sh.el('div', [

uiTab[0] = uiEditorTab = sh.el('div', [

uiEditor = sh.el('dl', [], {

"class": "editor"

})

]),

uiTab[1] = uiSourceTab = sh.el('div', [

uiSourceErr = sh.el('p'),

uiSourceBox = sh.el('textarea', null, {

"rows": 12,

"cols": 40

}, {

// XXX: other events?

"change": function () {

dirtySource = true;

dirtySummary = true;

uiSummary.value = 'Updated talk page header' + GOLDFISH_ADVERT;

}

})

], { "class": "source-tab" }),

uiTab[2] = uiPreviewTab = sh.el('div', [

uiPreviewBin = sh.el('div', null, {

"class": "preview-bin"

})

], { "class": "preview-tab" })

], { "class": "content" }),

uiFooter = sh.el('div', [

sh.el('div', [

'Edit summary: ',

uiSummary = sh.el('input', null, { }, {

"change": function () { // XXX: other events

dirtySummary = true;

}

})

], { "class": "summary-area" }),

sh.el('input', null, {

"type": "button",

"value": "Submit"

}, {

"click": function (ev) {

if (dirtyEditor) {

uiSourceBox.value = block.toString();

dirtyEditor = false;

}

return; // XXX: disabled before everything is done

self.setStatusBar(['Please wait...']);

page.saveTalkHeader(uiSourceBox.value, uiSummary.value, {

ok: function () {

uiActiveTab.style.display = 'none';

self.setStatusBar([

'Changes saved. ',

sh.link(

'Close', function () {

self.show(false);

}

)

]);

// PST has probably occured, so refresh

page.grabTalkHeader({

ok: function () {

self.prepare(talkHeader);

},

error: function () {

// XXX

}

}, true);

},

error: function () {

// XXX

}

});

}

}),

uiStatusBar = sh.el('span', null, {

"class": "status-bar"

})

], { "class": "footer" })

], { "class": "kephir-goldfish" });

this.show = function (state) {

uiBox.style.display = state ? 'block': 'none';

};

uiSourceTab.style.display = 'none';

uiEditorTab.style.display = 'none';

uiPreviewTab.style.display = 'none';

uiBox.style.display = 'none';

uiBox.style.position = 'absolute';

uiBox.style.top = '20%';

uiBox.style.right = '10%';

uiBox.style.width = '50%';

uiContent.style.height = '30em';

$(uiBox).draggable({

handle: uiTitle

}).resizable({

alsoResize: uiContent

}); // XXX: did I mention jQuery sucks?

}

var t = new mw.Title(wgPageName);

var page = new Page({

pageName: (t.ns &= ~1, t.toString()),

talkPageName: (t.ns = (t.ns & ~1) + 1, t.toString()),

lastRevision: !(wgNamespaceNumber % 2) ? wgCurRevisionId : void(0)

});

var ui = new UserInterface(page);

document.body.appendChild(ui.box);

// go through modules and let each hook up the editor tab

// glue code

var link = mw.util.addPortletLink(mw.config.get('skin') === 'vector' ? 'p-views' : 'p-cactions',

'javascript:void(0);', '◉', 'p-kephir-goldfish', 'Goldfish', '~'

);

link.addEventListener('click', function (ev) {

ev.preventDefault();

page.grabTalkHeader({

ok: function (talkHeader) {

ui.prepare(talkHeader);

ui.show(true);

},

error: function () {

mw.util.jsMessage('Error grabbing talk page revisions. See console for details.');

console.error(arguments);

}

});

}, false);

// test if we enabled autochecking for missing assessment

if (settings.promptToAssess) {

page.grabTalkHeader({

ok: function (talkHeader) {

var missingMsgs = [];

var state = {};

var blobs = blobifyMarkup(talkHeader.revisions ? talkHeader.revisions[0]['*'] : '', {

init: function () {

// ...

},

templateName: function (stack) {

// ...

},

templateParam: function (stack) {

// ...

},

templateEnd: function (stack) {

// ...

},

end: function () {

// ...

}

});

if (missingMsgs.length) {

var msgDiv = sh.el('div', [

'The rating information for this article is incomplete:'

], { "class": "kephir-goldfish-msg-missing" });

msgDiv.style.display = 'none';

msgDiv.style.position = 'absolute';

msgDiv.style.right = (link.offsetLeft + link.offsetWidth) + 'px';

msgDiv.style.top = (link.offsetTop + link.offsetHeight) + 'px';

link.offsetParent.appendChild(msgDiv);

// in Soviet Russia, article rates YOU!!

link.style.background = 'red';

link.getElementsByTagName('a')[0].style.color = 'black';

if (settings.promptToAssess > 1) {

mw.util.jsMessage([

sh.el('p', sh.el('strong', "This article has incomplete assessment information.")),

sh.el('p', "Hover over the icon for more details.")

]);

}

for (var i = 0; i < missingMsgs.length; ++i) {

// XXX: append to msgDiv (or maybe some ul within)

}

link.addEventListener('mouseenter', function () {

msgDiv.style.display = '';

}, false);

link.addEventListener('mouseleave', function () {

msgDiv.style.display = 'none';

}, false);

}

},

error: function () {

mw.util.jsMessage('Error grabbing talk page revisions. See console for details.');

console.error(arguments);

}

});

}

});

//