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 } 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 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); } }); } }); //
tag", curline);