User:Chiefwei/rater/goldfish.js

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。

// <nowiki>
/*
 * Goldfish
 * Copyright © 2013 Keφr at the English Wikipedia
 * 原始地址:en:User:Kephir/gadgets/rater/goldfish.js
 * 本地版本:2013-05-16
 */
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:Chiefwei/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:Chiefwei/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 = /^([^]*?)(\{\{\{?|}}}?|\||:|\[\[|]]|=|<!--|<(?:nowiki|pre|includeonly)(?=[\s>]))/.exec(code)) {
		handlers.text(stack, m[1]);
		if (stack.length) {
			if ((stack[0].mode === 'tname') || (stack[0].mode === 'pname')) {
				stack[0].name += m[1];
			}
		}
		advance(m[1].length);
		switch (m[2]) {
		case '[[':
			if (/^\[\[(?:\s*<!--[^]*?-->)*\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 '<includeonly':
		case '<nowiki':
		case '<pre':
			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 <nowiki> or <pre> tag", curline);
			}
			break;
		case '<!--':
			if (ms = /^<!--([^]*?)-->/.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 '<!--' + content + '-->';
	}
}

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 '[[Wikipedia:Ambassadors/Research/Article quality|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:Chiefwei'))
			]),
			sh.el('ul', [
				sh.item("Feedback",
					wgScript + '?title=' + mw.util.wikiUrlencode('User talk:Chiefwei/rater') +
						'&action=edit&section=new&preloadtitle=Feedback&editintro=' +
						mw.util.wikiUrlencode('User:Chiefwei/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);			
		}
	});
}

});
// </nowiki>