User:Chiefwei/rater/rater-t.js

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

//简体中文用户请使用 User:Chiefwei/rater/rater.js
//原始地址:en:User:Kephir/gadgets/rater.js 版本:2012-11-11
/*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;
	
	mw.loader.load('/wiki/User:Chiefwei/rater/rater.css?action=raw&ctype=text/css', 'text/css');
	
	/*
	== 程式碼 ==
	 */
	var api = new mw.Api();
	
	/*
	=== 模板資料 ===
	 */
	var raterData = {};
	var dataurl ='';
	function getRaterData(kind) {
		if (kind === 'default') {
			dataurl = mw.config.get('wgScript') + '?action=raw&ctype=application/json&maxage=86400&title=User:Chiefwei/rater/' + kind + '.js';
		} else {
			dataurl = mw.config.get('wgScript') + '?action=raw&ctype=application/json&maxage=86400&title=User:Sz-iwbot/rater/' + kind + '.json';
		}
		if (raterData[kind] === void(null)) {
			try {
				$.ajax({
					'url': dataurl,
					'dataType': 'json',
					'async': false,
					'success': function (data) {
						raterData[kind] = data;
					},
					'error': function (xhr, message) {
						mw.log.error(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-t');
			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;
						}
						mw.log.error(new Error(message));
					}
				});
			}  catch (e) {
				alert('獲取模板“' + name + '”資料錯誤:' + e.message + '。如果您力所能及請修復之。正在回滾至預設資料,可能未必準確。');
				projectData[name] = null;
			}
		}
		return projectData[name];
	}
		
	function wantedTemplate(name) {
		var projects = getRaterData('projects-t');
		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-t');
		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) {
				if(node)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|工具協助]])';
		}
	
		var lastTemplate;
		var contig;
	
		var uiProjList = el('datalist');
		(function () {
			var projects = getRaterData('projects-t');
			for (var key in projects) {
				for (var k in projects[key]) {
					var pname = projects[key][k] || 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|工具協助]])';
						}
					},
					'change': function () {
						tabIsFresh[0] = tabIsFresh[2] = false;
						if (!dirtySummary) {
							dirtySummary = true;
							uiSummary.classList.add('dirty');
							uiSummary.value = '修改討論頁頂部([[User:Chiefwei/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;
				mw.log.error(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', 'List', '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'
					},
					'class-ext-list': {
						'list': ['', 'Stub', 'Start', 'C', 'B', 'GA', 'A', 'FA', 'SL', 'List', 'CL', 'BL', 'AL', 'FL'],
						'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:Chiefwei/rater/data-editnotice&preload=User:Chiefwei/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 {
							mw.log.error(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'
	);
	if(link) {
	link.addEventListener('click', function (ev) {
		ev.preventDefault();
		api.get({
			action: 'query',
			prop: 'info|revisions',
			rvprop: 'timestamp|content',
			rvsection: 0,
			rvlimit: 1,
			rvdir: 'older',
			meta: 'tokens',
			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 = result.query.tokens.csrftoken;
				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,
						tags: 'rater'
					}, 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 () { });
	}
	
	});