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 '([^]*?)<\/\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&section=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); }	}); }

}); //