跳转到内容

User:Muhebbet/rater/rater.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// 個人使用,主要支援自己常用的幾個專題
// 复制修改自[[User:Chiefwei/rater/rater.js]]的2015-06-02T16:19:19Z版
// 原始地址:[[:en:User:Kephir/gadgets/rater.js]] 版本:2017-11-05
/*jshint shadow:true, latedef:true, boss:true, scripturl:true, loopfunc:true, undef:true */
/*global $, mw, importStylesheet, wgScript, wgNamespaceIds, wgFormattedNamespaces, wgNamespaceNumber, wgPageName, wgTitle, wgAction */
mw.loader.using(['mediawiki.api', 'mediawiki.Title', 'jquery.ui'], function () {
"use strict";
if (mw.config.get('wgNamespaceNumber') < 0)
	return;

importStylesheet('User:Chiefwei/rater/rater.css');

/*
== 代码 ==
 */
var api = new mw.Api();

/*
=== 模板数据 ===
 */
var raterData = {};
	
function getRaterData(kind) {
	if (raterData[kind] === void(null)) {
		try {
			$.ajax({
				'url': mw.config.get('wgScript') + '?action=raw&ctype=application/json&maxage=86400&title=User:Muhebbet/rater/' + kind + '.js',
				'dataType': 'json',
				'async': false,
				'success': function (data) {
					raterData[kind] = data;
				},
				'error': function (xhr, message) {
					throw new Error(message);
				}
			});
		}  catch (e) {
			alert('获取评级工具“' + kind + '”数据错误:' + e.message + '。评级工具可能无法正常工作。');
			raterData[kind] = null;
		}
	}
	return raterData[kind];
}

var projectKeywords = null;

function getKeywordsMapping() {
	if (projectKeywords === null) {
		var projects = getRaterData('projects');
		projectKeywords = {};
		for (var key in projects) {
			for (var i = 0; i < projects[key].length; ++i) {
				projectKeywords[projects[key][i].toLowerCase()] = key;
			}
		}
	}
	return projectKeywords;
}

function cloneInto(what, target) {
	if (typeof what === 'object') {
		if (what === null)
			return what;
		if ((target === null) || (typeof target !== 'object'))
			target = {};
		for (var key in what) {
			target[key] = cloneInto(what[key], target[key]);
		}
		return target;
	} else
		return what;
}

function clone(what) {
	return cloneInto(what, null);
}

var projectData = {};
function getTemplateInfo(name) {
	if (projectData[name] === void(null)) {
		try {
			$.ajax({
				'url': mw.config.get('wgScript') + '?action=raw&ctype=application/json&maxage=86400&title=Template:' + name + '/rater-data.js',
				'dataType': 'json',
				'async': false,
				'success': function (data) {
					projectData[name] = data;
				},
				'error': function (xhr, message) {
					if (xhr.status === 404) { // just pretend nothing happened.
						projectData[name] = null;
						return;
					}
					throw new Error(message);
				}
			});
		}  catch (e) {
			alert('获取模板“' + name + '”数据错误:' + e.message + '。如果您力所能及请修复之。正在回滚至默认数据,可能未必准确。');
			projectData[name] = null;
		}
	}
	return projectData[name];
}
	
function wantedTemplate(name) {
	var projects = getRaterData('projects');
	return name in projects;
}

function normaliseTitle(name) {
	var aliases = getRaterData('aliases');
	name = (new mw.Title(name)).getMainText();
	if (aliases[name])
		name = aliases[name];
	return name;
}

/*
=== 模板对象 ===
 */
function ProjectTemplate(name, params, postws) {
	var sumtrack = {};
	var isnew = (params === null);
	var dropped = false;
	var tpinfo = getTemplateInfo(name);
	var defdata = getRaterData('default');
	var fallback;

	if (tpinfo === null || typeof(tpinfo) === 'undefined') {
		tpinfo = {};
		fallback = true;
	}
	tpinfo = cloneInto(tpinfo, clone(defdata));

	// make data nice
	if (tpinfo.taskforces) {
		if (tpinfo.taskforces.items) {
			var newtf = [];
			for (var key in tpinfo.taskforces.items) {
				var tfe = {
					'name': tpinfo.taskforces.items[key],
					'part': (typeof tpinfo.taskforces.partsuf === 'string') ? key + tpinfo.taskforces.partsuf : null,
					'prio': (typeof tpinfo.taskforces.priosuf === 'string') ? key + tpinfo.taskforces.priosuf : null
				};
				newtf[newtf.length] = tfe;
			}
			tpinfo.taskforces = newtf;
		}
	} else
		tpinfo.taskforces = [];

	for (var key in tpinfo.params)
		if (tpinfo.params[key] === null)
			delete tpinfo.params[key];

	for (var i = 0; i < tpinfo.taskforces.length; ++i) {
		var tf = tpinfo.taskforces[i];
		if (tf.part) {
			tpinfo.params[tf.part] = cloneInto(tpinfo.params[tf.part] || {}, {
				'group': 'task'
			});
		}
		if (tf.prio) {
			tpinfo.params[tf.prio] = cloneInto(tpinfo.params[tf.prio] || {}, {
				'group': 'task'
			});
		}
		if (tf.prio && tf.part) {
			tpinfo.params[tf.prio].implies = tf.part;
		}
	}

	params = params || {};

	// process params
	for (var key in params) {
		if ((key in tpinfo.params) && (tpinfo.params[key].alias)) {
			params[tpinfo.params[key].alias] = params[key];
			delete params[key];
		}
	}

	this.isFallback = function () {
		return fallback;
	};

	this.getParam = function (key) {
		return key in params ? params[key] : null;
	};
	
	this.setParam = function (key, value) {
		if ((value === null) || (value === void(null)))
			return this.delParam(key);
		params[key] = String(value);
		sumtrack[key] = String(value);
		return value;
	};
	
	this.delParam = function (key) {
		delete params[key];
		sumtrack[key] = false;
		return void(null);
	};

	this.serialise = function () {
		if (dropped)
			return '';
		var result = '{{' + name;
		for (var key in params) {
			result += ' |' + key + '=' + params[key];
		}
		return result + '}}' + postws;
	};
	
	this.getChanges = function () {
		if (dropped)
			return isnew ? '' : '-' + this.getProjectName();
		var result = [];
		for (var key in sumtrack) {
			if (sumtrack[key] !== false)
				result[result.length] = key + '=' + sumtrack[key];
			else
				result[result.length] = '-' + key;
		}
		if (!result.length) {
			if (isnew)
				return '+' + this.getProjectName();
			return void(null);
		}
		return (isnew ? '+' : '') + this.getProjectName() + ': ' + result.join(", ");
	};
	
	this.getProjectName = function () {
		if (tpinfo.name)
			return tpinfo.name;
		return name.replace(/^WikiProject /, '');
	};
	
	this.getTemplateName = function () {
		return name;
	};
	
	this.getParamData = function (key) {
		var defParamData = {
			'desc': key,
			'group': (key in tpinfo.params) ? 'main' : 'unk',
			'mandatory': false,
			'obsolete': false,
			'values': 'string',
			'defvalue': (key in tpinfo.params) ?
				( (tpinfo.params[key].values === 'flag-temp') ? 'y'
				: (tpinfo.params[key].values === 'flag-perm') ? 'n'
				: ''
			) : ''
		};
		return cloneInto(tpinfo.params[key] || {}, defParamData);
	};
	
	this.drop = function () {
		return dropped = !dropped;
	};

	this.forEachParam = function (walker) {
		for (var key in tpinfo.params) {
			if (tpinfo.params[key].alias)
				continue;
			if (walker(key, key in params ? params[key] : null, this.getParamData(key)))
				return;
		}
		for (var key in params) {
			if (!(key in tpinfo.params))
				if (walker(key, params[key], this.getParamData(key)))
					return;
		}
	};
	
	this.forEachTaskForce = function (walker) {
		for (var i = 0; i < tpinfo.taskforces.length; ++i) {
			var tf = tpinfo.taskforces[i];
			if (walker(tf.name, this.getParam(tf.part), this.getParam(tf.prio), tf.part, tf.prio))
				return;
		}
	};
}

/*
=== 界面 ===
 */
function UserInterface() {
	if (this === window)
		return new UserInterface();

	var self = this;

	function el(tag, child, attr, events) {
		var node = document.createElement(tag);

		if (child) {
			if (typeof child !== 'object')
				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) {
			node.setAttribute(key, String(attr[key]));
		}

		if (events) for (var key in events) {
			node.addEventListener(key, events[key], false);
		}

		return node;
	}

	function link(child, href, attr, ev) {
		attr = attr || {};
		ev = ev || {};
		if (typeof href === 'string')
			attr.href = href;
		else {
			attr.href = 'javascript:void(null);';
			ev.click = href;
		}
		return el('a', child, attr, ev);
	}

	function pform(child, handler, attr) {
		attr = attr || {};
		attr.action = 'javascript:void(null);';
		return el('form', child, attr, { 'submit': handler });
	}
	
	var pluckedData = [];
	var tab = [null, null, null];
	
	var tabIsFresh = [false, false, false];
	var uiSource, uiPreview, uiStatus, uiTemplates, uiSummary, uiAddTemplName;
	var dirtySummary = false;
	
	var curTab = 0;
	function setTab(target) {
		tab[curTab].style.display = 'none';
		tab[curTab = target].style.display = '';
	}
	
	function switchTab(target) {
		if (!tabIsFresh[target]) {
			if (target === 2) { // preview
				var source = tabIsFresh[1] ? uiSource.value : serialise();
				api.post({
					'action': 'parse',
					'title': talkpage,
					'text': source,
					'pst': '1',
					'prop': 'text'
				}, {
					success: function (result) {
						uiPreview.innerHTML = result.parse.text['*']; // XXX
						setTab(target);
						tabIsFresh[2] = true;
					},
					error: function () {
						self.setStatus('渲染错误。');
					}
				});
				return;
			} else if (target === 1) { // source
				if (tabIsFresh[0]) {
					uiSource.value = serialise();
				}
			} else if (target === 0) { // editor
				try {
					self.extract(uiSource.value);
				} catch (e) {
					self.setStatus(e.message + ' — 请编辑源代码并重试。');
					return;
				}
			}
			tabIsFresh[target] = true;
		}
		
		setTab(target);
	}
	
	function tabSwitcher(target) {
		return function (ev) {
			switchTab(target);
		};
	}
	
	function serialise() {
		var result = '';
		for (var i = 0; i < pluckedData.length; ++i) {
			if (typeof pluckedData[i] === 'string')
				result += pluckedData[i];
			else if (pluckedData[i].serialise)
				result += pluckedData[i].serialise();
		}
		return result;
	}
	
	function gatherSummary() {
		var sums = [];
		for (var i = 0; i < pluckedData.length; ++i) {
			if (pluckedData[i].getChanges) {
				var ch = pluckedData[i].getChanges();
				if (ch) sums[sums.length] = ch;
			}
		}
		return '评级:' + sums.join('、') + ' ([[User:Chiefwei/rater|rater]])';
	}

	var lastTemplate;
	var contig;

	var uiProjList = el('datalist');
	(function () {
		var projects = getRaterData('projects');
		for (var key in projects) {
			var pname = projects[key][0] || key.replace(/^WikiProject /, '');
			uiProjList.appendChild(el('option', [pname], { 'value': pname }));
		}
	})();
	
	var uiBox = el('div', [
		// header
		el('h4', ['评级',
			el('span', ['\u00a0[', link('关闭', function (ev) {
				ev.preventDefault();
				self.show(false);
			}), ']'], { 'class': 'editsection' }),
			el('span', ['\u00a0[',
				'β (2012-11-11) | ',
				link('关于', mw.util.getUrl('en:User:Kephir/gadgets/rater'), { 'title': '关于本小工具' }), ' | ',
				link('汉化', mw.util.getUrl('User:Chiefwei/rater'), { 'title': '汉化者页面' }), ' | ',
				link('反馈', mw.util.getUrl('en:User talk:Kephir/gadgets/rater'), { 'title': '向作者反馈' }),
			']'], { 'class': 'editsection' })
		]),
			
		// tabs
		el('ul', [
			el('li', [link('编辑' , tabSwitcher(0))]),
			el('li', [link('源码' , tabSwitcher(1))]),
			el('li', [link('预览', tabSwitcher(2))])
		], { 'class': 'tabs' }),
			
		// body: main
		tab[0] = el('div', [
			uiTemplates = el('ul'),
			el('div', [
				'附注',
				el('ul', [
					el('li', [
						'部分专题模板(标示', el('span', '如此', { 'class': 'fallback' }),
						')缺少评级数据,故已使用默认数据,但可能无法正确映射模板所能识别的实际参数。例如,部分专题横幅未适用重要度或图片/信息框请求等字段,亦可能使用了特殊名称。请使用预览功能以检查参数是否受正确识别,并使用源代码编辑器修正,亦可点击[编辑]链接填写模板评级数据。添加其他非评级讨论页模板时,请将评级参数按照默认留空,并填写该模板所含字段。', link('请清除浏览器缓存', mw.util.getUrl('WP:BYPASS')), '以确保评级工具使用最新的模板数据。'
					])
				])
			], { 'class': 'notes' }),
			el('form', [
				uiAddTemplName = el('input', null, {
					'type': 'text',
					'size': '30',
					'placeholder': '专题名、模板名、关键词或缩写',
					'class': 'name'
				}),
				el('input', null, {
					'type': 'submit',
					'value': '添加'
				})
			], { 'action': 'javascript:void(0);', 'class': 'new-template' }, {
				'submit': function (ev) {
					var name = uiAddTemplName.value;
					if (!name)
						return;
					var keywords = getKeywordsMapping();
					if (keywords[name.toLowerCase()])
						name = keywords[name.toLowerCase()];
					name = normaliseTitle(name);
					if (!wantedTemplate(name))
						name = 'WikiProject ' + name;
					if (!wantedTemplate(name)) {
						if (!confirm('“' + uiAddTemplName.value + '”无法与已知专题关联。是否依然添加?'))
							return;
					}
					var ptpl = new ProjectTemplate(name, null, '\n');
					pluckedData.splice(++lastTemplate, 0, ptpl);
					uiTemplates.appendChild(createUIForProjectTemplate(ptpl, true));
					tabIsFresh[1] = tabIsFresh[2] = false;
					uiAddTemplName.value = '';
					if (!dirtySummary) {
						uiSummary.value = gatherSummary();
					}
				}
			})
		]),
			
		// body: source
		tab[1] = el('div', [
			uiSource = el('textarea', null, {
				'rows': 15,
				'cols': 70
			}, {
				'keypress': function () {
					tabIsFresh[0] = tabIsFresh[2] = false;
					if (!dirtySummary) {
						dirtySummary = true;
						uiSummary.classList.add('dirty');
						uiSummary.value = '修改讨论页顶部 ([[User:Chiefwei/rater|rater]])';
					}
				},
				'change': function () {
					tabIsFresh[0] = tabIsFresh[2] = false;
					if (!dirtySummary) {
						dirtySummary = true;
						uiSummary.classList.add('dirty');
						uiSummary.value = '修改讨论页顶部 ([[User:Chiefwei/rater|rater]])';
					}
				}
			})
		]),

		// body: preview
		tab[2] = el('div', [
			uiPreview = el('div')
		]),

		el('br', null, { 'class': 'before-footer' }),

		// footer
		el('div', [
			el('div', [
				el('label', '编辑摘要:'),
				uiSummary = el('input', null, {
					'type': 'text',
					'size': '60'
				}, { 'keypress': function (ev) {
					if (!dirtySummary) {
						dirtySummary = true;
						this.classList.add('dirty');
					}
				}})
			]),
			el('span', [uiStatus = document.createTextNode('')], { 'class': 'status-line' }),
			el('input', null, {
				'type': 'button',
				'value': '保存',
				'class': 'save-button'
			}, { 'click': function (ev) {
				var summary = uiSummary.value;
				var markup = (curTab === 0) ? serialise() : uiSource.value;
				self.setStatus('正在提交……');
				self.save(markup, summary, {
					success: function () {
						self.setStatus('已更新');
						setTimeout(function () {
							self.show(false);
						}, 1500);
					},
					error: function () {
						console.error(arguments);
						self.setStatus('错误');
					}
				});
			} }),
			el('br')
		], { 'class': 'bottom' }),
			
		// nothing
		null
	], { 'class': 'kephir-rater' });
	
	$(uiBox).draggable().resizable(); // XXX: jQuery sucks
	tab[0].style.display = tab[1].style.display = tab[2].style.display = 'none';

	function createUIForProjectTemplate(tp, expanded) {
		var paramWidget = {};
		
		function normaliseBool(value) {
			if (typeof value === 'boolean')
				return value;
			if ((value === '1') || (value === 'Y') || (value === 'y') || (value.toLowerCase() === 'yes'))
				return true;
			if ((value === '0') || (value === 'N') || (value === 'n') || (value.toLowerCase() === 'no'))
				return false;
			throw new Error("无法正常化布尔值");
		}
		
		function updateValue(param, value) {
			tabIsFresh[1] = tabIsFresh[2] = false;
			tp.setParam(param, value);
			if (!dirtySummary) {
				uiSummary.value = gatherSummary();
			}
		}

		var grph = { };
		function createWidget(name, value, data) {
			var widget;
			
			var dataTypes = {
				'flag-temp': function () {
					widget = el('input', null, { 'type': 'checkbox' });
					widget.checked = normaliseBool(value);
					widget.addEventListener('change', function () {
						updateValue(name, this.checked ? 'yes' : null);
					}, false);
				},
				'flag-perm': function () {
					widget = el('input', null, { 'type': 'checkbox' });
					widget.checked = normaliseBool(value);
					widget.addEventListener('change', function () {
						updateValue(name, this.checked ? 'yes' : 'no');
					}, false);
				},
				'flag-inv': function () {
					widget = el('input', null, { 'type': 'checkbox' });
					widget.checked = !normaliseBool(value);
					widget.addEventListener('change', function () {
						updateValue(name, this.checked ? 'no' : 'yes');
					}, false);
				},
				'string': function () {
					widget = el('input', null, { 'type': 'text' });
					widget.value = value;
					widget.addEventListener('change', function () {
						updateValue(name, this.value);
					}, false);
					widget.addEventListener('keypress', function () {
						updateValue(name, this.value);
					}, false);
				},
				'class-std': {
					'list': ['', 'Stub', 'Start', 'C', 'B', 'GA', 'A', 'FA', 'SL', 'List', 'CL', 'BL', 'AL', 'FL', 'Disambig', 'NA'],
					'normalise': 'mlc'
				},
				'class-ext': {
					'list': ['', 'Stub', 'Start', 'C', 'B', 'GA', 'A', 'FA', 'List', 'FL', 'NA', 'Category', 'Disambig', 'File', 'Portal', 'Project', 'Template'],
					'normalise': 'mlc'
				},
				'importance-std': {
					'list': ['', 'Low', 'Mid', 'High', 'Top', 'NA', 'Bottom', 'Unknown'],
					'normalise': 'mlc'
				},
				'importance-lit': {
					'list': ['', 'Low', 'Mid', 'High', 'Top'],
					'normalise': 'mlc'
				},
				'b-checklist': { 
					'list': ['', 'yes', 'no', 'n/a'], 
					'normalise': 'mlc' 
				}
			};
			var uiName = el('span', [data.desc]);
			var uiHelp = null;
			
			if (typeof data.values === 'string') {
				try {
					if (typeof dataTypes[data.values] === 'function')
						dataTypes[data.values]();
					else if (dataTypes[data.values])
						data.values = dataTypes[data.values];
					else
						dataTypes.string();
				} catch (e) {
					dataTypes.string();
				}
			}

			if (data.values.list) {
				widget = el('select');
				
				var vlist;
				if (data.values.list instanceof Array) {
					vlist = data.values.list;
					for (var i = 0; i < vlist.length; ++i) {
						widget.appendChild(el('option', [vlist[i]], { 'value': vlist[i] }));
					}
				} else {
					vlist = Object.keys(data.values.list);
					for (var key in data.values.list) {
						widget.appendChild(el('option', [data.values.list[key]], { 'value': key }));
					}
				}
				
				if (data.values.normalise) {
					value = value || '';
					switch (data.values.normalise) {
					case 'lc':
						value = value.toLowerCase();
						break;
					case 'mlc':
						for (var i = 0; i < vlist.length; ++i) {
							if (vlist[i].toLowerCase() === value.toLowerCase()) {
								value = vlist[i];
								break;
							}
						}
						break;
					}
				}

				if (data.values.aliases) {
					if (value in data.values.aliases) {
						value = data.values.aliases[value];
					}
				}

				if (vlist.indexOf(value) === -1) {
					dataTypes.string();
					// TODO: datalist
				} else {
					widget.value = value;
					widget.addEventListener('change', function () {
						updateValue(name, this.value);
					}, false);
				}
			}

			// TODO: datalist

			if (data.obsolete) {
				uiName.classList.add('obsolete');
				uiName.title = 'This parameter is obsolete.';
			}
			
			if (data.helplink) {
				uiHelp = el('span', ['[', link('?', mw.util.getUrl(data.helplink)), ']']);
			}

			paramWidget[name] = widget;
			return el('li', [uiName, uiHelp && ' ', uiHelp, ':', widget]);
		}
		
		function createPlaceholder(name, data) {
			var pholder = el('li', [paramWidget[name] = link(data.desc, function (ev) {
				tabIsFresh[1] = tabIsFresh[2] = false;
				tp.setParam(name, data.defvalue);
				if (grph[data.group])
					grph[data.group].classList.remove('absent');
				var widget = createWidget(name, data.defvalue, data);
				pholder.parentNode.insertBefore(widget, pholder);
				pholder.parentNode.removeChild(pholder);
				if (!dirtySummary) {
					uiSummary.value = gatherSummary();
				}
			})], { 'class': 'absent' });
			return pholder;
		}
		
		function createWidgetTF(tfname, part, prio, partpname, priopname, prioscale) {
			var uiCheck = null, uiCombo = null;

			if (partpname !== null) {
				uiCheck = el('input', null, { 'type': 'checkbox' });
				try {
					uiCheck.checked = normaliseBool(part);
				} catch (e) {
					uiCheck.checked = true;
				}
				uiCheck.addEventListener('change', function (w) {
					updateValue(partpname, this.checked ? 'yes' : null);
				}, false);
				paramWidget[partpname] = uiCheck;
			}

			if (priopname !== null) {
				var items = prioscale || ["", "Top", "High", "Mid", "Low", "Unknown"]; // XXX
				uiCombo = el('select');
				for (var i = 0; i < items.length; ++i) {
					uiCombo.appendChild(el('option', [items[i]], { 'value': items[i] }));
				}
				uiCombo.value = prio;
				uiCombo.addEventListener('change', function () {
					updateValue(priopname, this.value);
				}, false);
				paramWidget[priopname] = uiCombo;
			}

			return el('li', [uiCheck, tfname, uiCombo && ': ', uiCombo]);
		}

		function createPlaceholderTF(tfname, partpname, priopname) {
			var pholder = el('li', [ paramWidget[priopname] = paramWidget[partpname] = link(tfname, function (ev) {
				if (partpname) tp.setParam(partpname, 'y');
				if (priopname) tp.setParam(priopname, '');
				var widget = createWidgetTF(tfname, true, '', partpname, priopname);
				grph.task.classList.remove('absent');
				pholder.parentNode.insertBefore(widget, pholder);
				pholder.parentNode.removeChild(pholder);
				if (!dirtySummary) {
					uiSummary.value = gatherSummary();
				}
			})], { 'class': 'absent' });
			return pholder;
		}

		var uiParams, uiGroups, uiDel;
		var ui = el('li', [
			uiDel = link('(delete)', function () {
				if (tp.drop())
					ui.classList.add('dropped');
				else
					ui.classList.remove('dropped');
				tabIsFresh[1] = tabIsFresh[2] = false;
				if (!dirtySummary)
					uiSummary.value = gatherSummary();
			}, { 'class': 'delete-template' }),
			el('b', [link(tp.getProjectName(), mw.util.getUrl('Template:' + tp.getTemplateName()))]),
			el('small', [' [', link('编辑', '/wiki/Template:' + tp.getTemplateName() + '/rater-data.js?action=edit&editintro=User:Muhebbet/rater/data-editnotice&preload=User:Muhebbet/rater/data-preload'), ']'], { 'class': 'absent', 'title': 'Edit data' }), ':',
			uiParams = el('ul', [], { 'class': 'params' }),
			uiGroups = el('dl', [], { 'class': 'p-groups' })
		], { 'class': 'template-entry' });
		var grpNames = {
			'vis' : '外观',
			'req' : '请求',
			'task': '工作组',
			'misc': '其他',
			'unk' : '未识别'
		};
		var grps = {
			'main': uiParams
		};
		var grpl = {
			'main': null
		};
		
		function addGroup(tag) {
			uiGroups.appendChild(grph[tag] = el('dt', grpNames[tag] || tag, { 'class': 'absent' }));
			uiGroups.appendChild(el('dd', [grps[tag] = el('ul', null, { 'class': 'params' })]));
			return grps[tag];
		}

		if (!expanded) {
			ui.classList.add('hide-absent');
		}

		if (tp.isFallback()) {
			ui.classList.add('fallback');
		}
		
		tp.forEachParam(function (name, value, data) {
			if (data.group === 'task')
				return;
			if (!grps[data.group])
				addGroup(data.group);
			if ((value === null) && !data.mandatory)
				if (!data.obsolete)
					grps[data.group].appendChild(createPlaceholder(name, data));
				else;
			else {
				grps[data.group].appendChild(createWidget(name, value, data));
				if (grph[data.group])
					grph[data.group].classList.remove('absent');
			}
		});
		
		tp.forEachTaskForce(function (tfname, part, prio, partpname, priopname) {
			var item;
			if (!grps.task) addGroup('task');
			if (partpname && (part === null))
				item = createPlaceholderTF(tfname, partpname, priopname);
			else {
				item = createWidgetTF(tfname, part, prio, partpname, priopname);
				grph.task.classList.remove('absent');
			}
			grps.task.appendChild(item);
		});
		
		var uiNewCustParmName;
		ui.appendChild(el('div', [
			el('form', [
				uiNewCustParmName = el('input', null, { 'type': 'text', 'placeholder': '新自定义参数', 'size': 20 }),
				el('input', null, { 'type': 'submit', 'value': '添加' })
			], { 'action': 'javascript:void(0);', 'class': 'new-custom-param' }, {
				'submit': function (ev) {
					var nname = uiNewCustParmName.value;
					tabIsFresh[1] = tabIsFresh[2] = false;
					if (paramWidget[nname]) {
						if (paramWidget[nname].tagName === 'A') { // XXX - placeholder
							paramWidget[nname].click();
						} else {
							// reactivate if deleted (deleting not implemented yet)
							paramWidget[nname].focus();
						}
					} else {
						var pd = tp.getParamData(nname);
						tp.setParam(nname, '');
						(grps[pd.group] || addGroup(pd.group)).appendChild(createWidget(nname, '', pd));
						if (!dirtySummary) {
							uiSummary.value = gatherSummary();
						}
					}
					uiNewCustParmName.value = '';
				}
			})
		], { 'class': 'absent' }));

		var hidelink, hltext;
		uiParams.appendChild(hidelink = link([hltext = document.createTextNode('[+]')], function () {
			if (!ui.classList.contains('hide-absent')) {	
				ui.classList.add('hide-absent');
				hltext.data = '[+]';
			} else {
				ui.classList.remove('hide-absent');
				hltext.data = '[–]';
			}
		}));

		return ui;
	}
	
	this.clear = function () {
		dirtySummary = false;
		uiSummary.value = '';
		uiSummary.classList.remove('dirty');
	};

	this.extract = function (markup) {
		var m;

		pluckedData = [];
		lastTemplate = -1;
		contig = true;

		while (uiTemplates.hasChildNodes())
			uiTemplates.removeChild(uiTemplates.firstChild);
		
		uiSource.value = markup;
		tabIsFresh[1] = true;

		try {
			// TODO: Parsoid?
			while (markup !== '') {
				if (!(m = /^([^]*?)\{\{\s*((?:\}[^\}\|]|[^\}\|])+?)\s*(?=\||}})/.exec(markup))) {
					pluckedData[pluckedData.length] = markup;
					break;
				}
				var name = normaliseTitle(m[2]);
				if (!wantedTemplate(name)) {
					pluckedData[pluckedData.length] = markup.substr(0, m[0].length);
					markup = markup.substr(m[0].length);
					continue;
				}
				pluckedData[pluckedData.length] = m[1];
				var params = {};
				var postws = '';
				markup = markup.substr(m[0].length);
				var ppid = 1;

				for (;;) {
					if (m = /^\s*}}(\s*)/.exec(markup)) {
						postws = m[1];
						markup = markup.substr(m[0].length);
						break;
					} else if (m = /^\s*\|\s*([^=\|]+?)\s*=\s*(.*?)\s*(?=\||}})/.exec(markup)) {
						markup = markup.substr(m[0].length);
						params[m[1]] = m[2];
					} else if (m = /^\s*\|\s*(.*?)\s*(?=\||}})/.exec(markup)) {
						markup = markup.substr(m[0].length);
						params[ppid++] = m[1];
					} else {
						throw new Error('“' + name + '”错误的模板调用');
					}
				}
				
				if (lastTemplate !== (pluckedData.length - 1))
					contig = false;

				var ptpl = new ProjectTemplate(name, params, postws);
				pluckedData[lastTemplate = pluckedData.length] = ptpl;
				uiTemplates.appendChild(createUIForProjectTemplate(ptpl));
			}

			tabIsFresh[0] = true;
			setTab(0);
		} catch (e) {
			this.setStatus('解析' + e.message + '时出错,请修复源代码并重试。');
			setTab(1);
		}
	};
		
	this.save = function (markup, summary, handlers) {
		alert('@!#?@!\n' + summary + '\n' + markup);
	};

	this.show = function (value) {
		uiBox.style.display = value ? '' : 'none';
		if (value) {
			if (curTab === 0)
				uiAddTemplName.focus();
			else if (curTab === 1)
				uiSource.focus();
		}
	};
	
	this.install = function (where) {
		function uniqid() {
			var s = '', cs = 'QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890.-';
			for (var i = 0; i < 24; ++i) {
				s += cs.charAt(Math.floor(Math.random() * cs.length));
			}
			return s;
		}
		where.appendChild(uiBox);
		where.appendChild(uiProjList);
		uiProjList.id = 'kephir-rater-' + uniqid();
		uiAddTemplName.setAttribute('list', uiProjList.id);
	};
	
	this.setStatus = function (message, level) {
		uiStatus.data = message;
	};
	
	return this;
}

/*
=== Glue ===
 */
var api = new mw.Api();

var ui = new UserInterface();
ui.show(false);
ui.install(document.body);

var talkpage = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceNumber') - (mw.config.get('wgNamespaceNumber') % 2) + 1] + ':' + mw.config.get('wgTitle');

var link = mw.util.addPortletLink(mw.config.get('skin') === 'vector' ? 'p-views' : 'p-cactions',
	'javascript:void(0);', '评级', 'p-kephir-rater', '使用工具为条目评级', '5'
);
link.addEventListener('click', function (ev) {
	ev.preventDefault();
	api.get({
		action: 'query',
		prop: 'info|revisions',
		rvprop: 'timestamp|content',
		rvsection: 0,
		rvlimit: 1,
		rvdir: 'older',
		intoken: 'edit',
		titles: talkpage
	}, {
		success: function (result) {
			var tpgpid = Object.keys(result.query.pages)[0];
			var tpg = result.query.pages[tpgpid];
			var tpgstart = tpg.starttimestamp;
			var tpgtoken = tpg.edittoken;
			var tpgbase = tpg.revisions ? tpg.revisions[0].timestamp : void(0);
			var tpgrev = tpg.lastrevid;
			ui.save = function (markup, summary, handlers) {
				api.post({
					action: 'edit',
					section: 0,
					title: talkpage,
					basetimestamp: tpgbase,
					starttimestamp: tpgstart,
					token: tpgtoken,
					notminor: true,
					summary: summary,
					watchlist: 'nochange',
					text: markup
				}, handlers);
			};
			ui.clear();
			ui.extract(tpg.revisions ? tpg.revisions[0]['*'] : '');
			ui.show(true);
		},
		error: function () {
			console.error(arguments);
			alert('错误,参见控制台。');
		}
	});
}, false);

if (/^Category:(Unassessed|Unknown-importance)_.*?_articles$/.test(mw.config.get('wgPageName'))) {
	var links = document.getElementById('mw-pages').getElementsByTagName('a');
	for (var i = 0; i < links.length; ++i)
		links[i].href = links[i].href.replace(/\/wiki\/Talk:/, '/wiki/');
}

if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').template) && /\/rater-data\.js$/.test(mw.config.get('wgPageName'))) {
	mw.config.set('wgCodeEditorCurrentLanguage', 'json');
	mw.loader.load('ext.codeEditor');
	if (Object.defineProperty)
		Object.defineProperty(window, 'syntaxHighlighterConfig', {
			'set': function () { }
		});
	else if (window.__defineSetter__)
		window.__defineSetter__('syntaxHighlighterConfig', function () { });
}

});