Pengguna:Kephir/gadgets/xte.js

Daripada Wiktionary

Catatan: Selepas menyiarkan perubahan, anda mungkin perlu memintas cache pelayar untuk melihatnya.

  • Firefox / Safari: Tahan Shift sambil mengklik Reload, atau tekan sama ada Ctrl-F5 atau Ctrl-R (⌘-R pada Mac)
  • Google Chrome: Tekan Ctrl-Shift-R (⌘-Shift-R pada Mac)
  • Internet Explorer / Edge: Tahan Ctrl sambil mengklik Refresh, atau tekan Ctrl-F5
  • Opera: Tekan Ctrl-F5.
// more information: [[User:Kephir/gadgets/xte]]
/*jshint shadow:true, scripturl:true, undef:true, latedef:true, boss:true, loopfunc:true, unused:true */ // <nowiki>
/*global mw, jQuery */
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.Uri'], function() {
"use strict";

var code2nameLookup = null;
var code2scLookup = null;

var name2codeLookup = {
	// "false" values indicate translation groups, i.e. names with no own code under which
	// distinct languages are kept together, like Chinese which contains the Mandarin (cmn),
	// Cantonese (yue) and Min Nan (nan) varieties as sub-items. xte warns when translations
	// are found directly under group headers, and ignores them otherwise.
	// Greek (el) for example should not be added here, because translations directly under
	// "Greek" item are accepted, even though "Greek" serves as a parent for Ancient Greek (grc)
	"Apache"               : false,
	"Sorbian"              : false,
	"Berber"               : false,
	"Sami"                 : false,
	"Marquesan"            : false,
	"Miwok"                : false,

	// nulls are explicit negative entries to avtoid having xte round-trip
	// to servers with known bad names. why does it even try these? when encountering
	// a translation sub-item, xte prepends the sub-item name before the item name.
	// this works well for "Egyptian" (otherwise egy) under "Arabic" (otherwise ar)
	// = "Egyptian Arabic" (arz), but it also results in "Lower Sorbian Sorbian".
	"Lower Sorbian Sorbian": null,
	"Upper Sorbian Sorbian": null
};

var wgPageName = mw.config.get('wgPageName');

jQuery.ajax({
	'url': mw.config.get('wgServer') + mw.config.get('wgScript') + '?action=raw&templates=expand&ctype=text/javascript&title=User:Kephir/gadgets/xte/langdata.js',
	'cache': true,
	'dataType': 'json',
	'success': function (data) {
		try {
			code2nameLookup = {};
			code2scLookup = {};
			for (var key in data) {
				code2nameLookup[key] = data[key].names[0];
				code2scLookup[key] = data[key].scripts[0];
			}
			for (var key in code2nameLookup) {
				name2codeLookup[code2nameLookup[key]] = key;
			}
		} catch (e) {
			mw.util.jsMessage('xte: error while processing language data. See console for details.');
			throw e;
		}
	},
	'error': function (xhr, message) {
		if (message !== 'abort')
			mw.util.jsMessage('xte: failed to grab language data. See console for details.');
		console.error(arguments);
	}
});

/*
=== Scripts ===
 */
var scname2scLookup = {
	"Roman"    : "Latn",
	"Latin"    : "Latn",
	"Cyrillic" : "Cyrl",
	"Mandarin" :  null,
	"Cantonese":  null
};

function code2name(code) {
	if (code in code2nameLookup)
		return code2nameLookup[code];
	else {
		var api = new mw.Api();
		var result = null;
		api.get({
			'action': 'expandtemplates',
			'text': '{{#invoke:languages/templates|getByCode|' + code + '|getCanonicalName}}'
		}, {
			async: false,
			success: function (obj) {
				result = obj.expandtemplates['*'];
				if (/class="error"/.test(result))
					result = null;
				code2nameLookup[code] = result;
			}
		});
		return result;
	}
}

function name2code(name) {
	if (name in name2codeLookup)
		return name2codeLookup[name];
	else {
		var api = new mw.Api();
		var result = null;
		api.get({
			'action': 'expandtemplates',
			'text': '{{langrev|' + name + '}}'
		}, {
			async: false,
			success: function (obj) {
				result = obj.expandtemplates['*'];
				if (result === '')
					result = null;
				if (/^\[\[/.test(result))
					result = null;
				if (/class="error"/.test(result))
					result = null;
				name2codeLookup[name] = result;
			}
		});
		return result;
	}
}

function code2sc(code) {
	if (code in code2scLookup)
		return code2scLookup[code];
	else {
		console.warn('script for "' + code + '" is missing from the lookup table; falling back to API query');
		var api = new mw.Api();
		var result = null;
		api.get({
			'action': 'expandtemplates',
			'text': '{{#invoke:languages/templates|getByCode|' + code + '|getScripts|1}}'
		}, {
			async: false,
			success: function (obj) {
				result = obj.expandtemplates['*'];
				if (/class="error"/.test(result))
					result = null;
				code2scLookup[code] = result = obj.expandtemplates['*'];
			}
		});
		return result;
	}
}

function scname2sc(scname) {
	if (scname in scname2scLookup)
		return scname2scLookup[scname];
	else {
		console.warn('script code for "' + scname + '" is missing from the lookup table; falling back to API query');
		var api = new mw.Api();
		var result = null;
		api.get({
			'action': 'expandtemplates',
			'text': '{{scriptrev|' + scname + '}}'
		}, {
			async: false,
			success: function (obj) {
				result = obj.expandtemplates['*'];
				if (result === '')
					result = null;
				scname2scLookup[scname] = result;
			}
		});
		return result;
	}
}

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

	if (child) for (var i = 0; i < child.length; ++i) {
		var ch = child[i];
		if (typeof ch === 'string')
			ch = document.createTextNode(ch);
		else if ((ch === null) || (ch === void(null)))
			continue;
		node.appendChild(ch);
	}

	if (attr) for (var key in attr) {
		node.setAttribute(key, attr[key]);
	}

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

	return node;
}

function ih(name, val) {
	var it = el('input', null, { 'type': 'hidden', 'name': name });
	if (val)
		it.value = val;
	return it;
}

function count(subs, s) {
	var i = 0, j = -subs.length;
	while ((j = s.indexOf(subs, j + subs.length)) !== -1) ++i;
	return i;
}

function fix(wikicode, susp, errors) {
	var i, dirty = false;
	var name = null, code = null, before, subbef;
	function addError(message) {
		errors[errors.length] = {
			'line': i + 1,
			'code': code,
			'mesg': message
		};
	}
	var m, lines = wikicode.split('\n');
	var mode = 0;
	if (!(susp instanceof Array))
		susp = [];
	if (!(errors instanceof Array))
		errors = [];
	for (i = 0; i < lines.length; ++i) {
		var ttbcline = false, issub = false, entrysc = '';
		if (m = /^\s*\{\{(checktrans|ttbc)-top(\||}})/.exec(lines[i])) {
			name = code = null;
			mode = 2;
			continue;
		} else if (m = /^\s*\{\{(checktrans|trans|ttbc)-bottom(\||}})\s*/.exec(lines[i])) {
			name = code = null;
			if (m[0] !== lines[i])
				addError('Stray markup after translation table end');
			mode = 0;
			continue;
		} else if (m = /^\s*\{\{trans-top(?:-also)?(\||}})\s*/.exec(lines[i])) {
			name = code = null;
			mode = 1;
			continue;
		}
		if (mode === 0)
			continue;
		if (m = /^\*(?![\*:])\s*(?:\[\[)?([^:\{]+?)(?:\]\])?\s*:\s*/.exec(lines[i])) {
			before = subbef = m[1];
			name = m[1];
			code = name2code(m[1]);
			if (code === false) { // language group, "Sorbian", which serves as a group for "Lower Sorbian" and "Upper Sorbian"
				if (lines[i] !== m[0]) {
					addError('Language group "' + m[1] + '" has direct items; these should be placed under a more specific header.');
					susp.push('"' + m[1] + '"');
					lines[i] = '* {{ttbc|' + m[1] + '}} {{attention|und|Wiktionary considers this a language group; please specify the language more precisely}}: ' + lines[i].substr(m[0].length);
					dirty = true;
					continue;
				} else continue;
			}
			if (!code) {
				if ((name === 'Serbian') || (name === 'Croatian') || (name === 'Bosnian')) {
					ttbcline = true;
					addError('Warning: converting "' + name + '" into "Serbo-Croatian" [sh].');
					name = 'Serbo-Croatian';
					subbef = 'Serbo-Croatian {{attention|sh|was "' + name + '"; verify correctness, check for duplicates, mark script, sort and merge if appropriate}}:';
					code = 'sh';
					susp.push("sh");
				} else {
					code = name = null;
					addError('"' + m[1] + '" does not refer to a known language.');
					susp.push('"' + m[1] + '"');
					lines[i] = '* {{ttbc|' + m[1] + '}} {{attention|und|name not recognised by xte: update [[Template:langrev]] and [[Module:languages]] appropriately or report to [[User talk:Kephir/gadgets/xte]]}}: ' + lines[i].substr(m[0].length);
					dirty = true;
					continue;
				}
			}
		} else if (m = /^\*\s*(\{\{ttbc\s*\|([^|]*?)}})(\s*\{\{\s*attention\s*\|.*?}})?\s*:\s*/.exec(lines[i])) {
			before = subbef = m[1];
			code = m[2];
			name = code2name(m[2]);
			ttbcline = true;
			if (!name) {
				if (code = name2code(m[2])) {
					name = m[2];
					before = subbef = '{{ttbc|' + code + '}}';
				} else {
					code = name = null;
					addError('"' + m[2] + '" under ttbc does not refer to a known language.');
					susp.push('"' + m[2] + '"');
					continue;
				}
			}
		} else if (m = /^\*[*:]\s*(?:\[\[)?([^:\{<]+?)(?:\]\])?\s*:\s*/.exec(lines[i])) {
			subbef = m[1];
			var s;
			if (s = scname2sc(subbef)) {
				if (code === null)
					continue;
				entrysc = s;
			} else if (s = name2code(subbef + ' ' + name)) { // prepend sub-item name before group name: "Ancient" + "Greek", "Lower" + "Sorbian", etc.
				code = s;
				name = subbef;
			} else if (s = name2code(subbef)) {
				code = s;
				name = subbef;
			} else if (s = name2code(subbef + ' ' + before)) {
				code = s;
				name = subbef;
			} else {
				if (subbef === 'Teochew') {
					ttbcline = true;
					code = 'nan'; 
					addError('Warning: treating "Teochew" header as if it were Min Nan [nan]. If you want to merge it with Min Nan, use {{qualifier}}.');
					susp.push('nan (Teochew)');
				} else {
					addError('Failed to recognize "' + subbef + '" sub-item for "' + before + '"');
					lines[i] = '*: {{ttbc|' + subbef + '}}<!-- name not recognised by xte: update [[Template:scriptrev]] or [[Template:langrev]] and [[Module:languages]] appropriately or report to [[User talk:Kephir/gadgets/xte]] -->: ' + lines[i].substr(m[0].length);
					dirty = true;
					continue;
				}
			}
			issub = true;
		} else if (m = /^\s*\{\{(checktrans|trans|ttbc)-mid(\||}})\s*/.exec(lines[i])) {
			name = code = null;
			if (m[0] !== lines[i])
				addError('Stray markup after translation table midbreak');
			continue;
		} else if (m = /^\*\s*\{\{trreq\|(.*?)}}\s*?(?=\s|$)/.exec(lines[i])) {
			name = code = null;
			if (!(name = code2name(m[1]))) {
				if (!(code = name2code(m[1]))) {
					addError('"' + m[1] + '" under {{trreq}} is not recognised');					
				} else {
					if (m[0] !== lines[i])
						addError('Stray markup after {{trreq}}');
					lines[i] = '* {{trreq|' + code + '}}' + lines[i].substr(m[0].length);
					continue;
				}
			}
			if (m[0] !== lines[i])
				addError('Stray markup after {{trreq}}');
			continue;
		} else {
			addError('Line not recognised');
			continue;
		}
		var parsed = lines[i].substr(m[0].length);
		var token = '';
		var t9ns = [];
		var or = 0, oc = 0, os = 0, ot = 0;
		while (parsed !== '') {
			if (!(m = /\s*[,;\/]\s*/.exec(parsed))) {
				t9ns[t9ns.length] = token + parsed;
				break;
			}
			var pre = parsed.substr(0, parsed.indexOf(m[0]));
			or += count('(', pre) - count(')', pre);
			oc += count('{', pre) - count('}', pre);
			os += count('[', pre) - count(']', pre);
			ot += count('<', pre) - count('>', pre);
			if ((or === 0) && (oc === 0) && (os === 0) && (ot === 0)) {
				t9ns[t9ns.length] = token + pre;
				token = '';
			} else {
				token += pre + m[0];
			}
			parsed = parsed.substr(parsed.indexOf(m[0]) + m[0].length);
		}
		var allok = true;
		for (var j = 0; j < t9ns.length; ++j) {
			if (/\{\{\s*(t[-+]check|t-needed)\|/.test(t9ns[j]))
				continue;
				
			var suspc = (mode === 2) || ttbcline;

			// validate (rudimentary!)
			if (!t9ns[j].replace(/<!--(.*?)-->/g, '').replace(/\s*\{\{t\+?\|(\|tr=\{\{IPAchar\|[^}|]+}}|[^}])+}}\s*/, '').replace(/\s*\{\{(qualifier|i)\|[^}]+}}\s*/g, '')) {
				if (suspc) {
					t9ns[j] = t9ns[j].replace(/\{\{t(\+?)\|/, function (m, t) {
						return '{{t' + (t ? '+' : '-') + 'check|';
					});
				}
				continue;
			}
			if (/\{\{t/.test(t9ns[j])) {
				addError('Translation item "' + t9ns[j] + '" failed to validate despite containing {{t}}. Will reconstruct.');
				suspc = true;
			}

			var inp = t9ns[j];
			// validation failed. deconstruct {{t}}s
			inp = inp.replace(/\{\{t\+?(\|[^|}]+\|[^|}]+)((\|tr=\{\{IPAchar\|[^}|]+}}|\|[^}|]+)*)}}(?!\))/g, function (m, ct, rest) {
				var newrest = '', g = '', tr = '';
				while (rest) {
					var r = rest.replace(/^(\|[^|=]+)+(?=\||$)/, function (m) {
						g += m;
						return '';	
					}).replace(/^\|alt=([^|}]+)/, function (m, a) {
						if (newrest)
							return m;
						newrest = '|' + a;
						return '';
					}).replace(/^\|sc=([^|}]+)/, '');
					if (r === rest)
						return m; // failed to deconstruct {{t}}
					rest = r;
				}
				return '{{l' + ct + newrest + '}}' + (tr ? ' (' + tr + ')' : '') + (g ? ' {{g' + g + '}}' : '');
			});
			
			if (/\{\{t/.test(inp)) {
				addError('Deconstruction failed for "' + t9ns[j] + '". Please fix it manually.');
				allok = false;
				continue;
			}

			console.info('transforming ', t9ns[j]);
			var qual = '', word = '', gend = '', tr = '', sc = entrysc, alt = '';
			inp = inp.replace(/\{\{l[\|/]([a-z\-]+)\|([^=|}]+(?:\|[^=|}]+)?)}}/g, function (m, lc, cont) {
				if (lc !== code) {
					// I have never actually encountered this, but just to be safe...
					addError('Warning: {{l}} template used with non-matching language code ' + lc);
					suspc = true;
					return m;
				}
				return '[[' + cont + ']]';
			});
			if (m = /\s*\{\{(?:qualifier|i)\|(.+?)}}\s*/.exec(inp)) {
				qual = '; ' + m[1];
				inp = inp.replace(m[0], '');
			} else {
				inp = inp.replace(/\s*(?:\(''|''\()([A-Za-z,;:'"\- ]+?)(?:''\)|\)'')/g, function(m, inside) {
					console.info('probably qualifier: ', arguments);
					qual += '; ' + inside;
					return '';
				});
			}
			var getWord = function (m) {
				var n;
				console.info('words: ', m);
				if (!/^\s*(\[\[[^\]\|]*?]]|[^ \[\]\{\}'<>]+)$/.test(m)) {
					var n = /^([^\[\{]*)\[\[([^#\|\]]*?)(?:#([^\|\]]*?))?(?:\|([^#\]]*?))?]]([^\[\{]*)$/.exec(m);
					if (n) {
						if (!(((n[2] === n[4]) || !n[2]) && (n[3] === name))) {
							addError('Warning: single piped or non-clean link in an item "' + m + '". Assuming an inflected form or vocalised spelling. See [[User:Kephir/gadgets/xte#Translation_fixing]].');
							suspc = 'was "' + m + '" - assumed inflected form or vocalised spelling; please verify if linking is acceptable. see [[User:Kephir/gadgets/xte#Translation_fixing]]';
							susp.push(code);
							alt = n[1] + (n[4] || n[2] || wgPageName) + n[5];
							word = n[2] || wgPageName;
							return '';
						}
					} else {
						addError('Warning: multiple words in an item: "' + m + '". Assuming it is a [[WT:SOP|sum-of-parts]]. If it is not, remove the links to individual words.');
						word = m;
						if (!/\[\[.*?]]/.test(word)) {
							word = word.replace(/([^{}\[\]\(\) \t,]+)/g, function (wm) {
								return '[[' + wm + ']]';
							});
						}
						suspc = 'was "' + m + '" - assumed sum-of-parts; if an idiom, remove the wikilinks to the individual words. see [[User:Kephir/gadgets/xte#Translation_fixing]]';
						susp.push(code);
						return '';
					}
				}
				word = m.replace(/\[\[(?:[^|\]]+?\|)?(.+?)]]/g, '$1').replace(/\s+/g, ' ');
				return '';
			};
			if (m = /^\{\{((?:[a-z][a-z]-)?[A-Z][a-z][a-z][a-z])\s*\|\s*(.+?)\s*}}/.exec(inp)) {
				sc = m[1];
				getWord(m[2]);
				inp = inp.replace(m[0], '');
			} else
				inp = inp.replace(/^((\s*)(?!''|<!--)(?:\[\[(?:[^|\]]+?\|)?(.+?)]]|([^\{\(\[<' ]+)))+/, getWord);
			if (/^\s*$/.test(word)) {
				addError('Failed to process item: "' + t9ns[j] + '"');
				allok = false;
				continue;
			}
			if (!sc)
				sc = code2sc(code);
			if (sc !== 'Latn') {
				inp = inp.replace(/\{\{(IPAchar)\|\(?(.+?)\)?}}/, function (m, w, t) {
					console.info('most probably transcription: ', arguments, '; script: ', sc);
					tr = '{{' + w + '|' + t + '}}';
					return '';
				});
			}	
			inp = inp.replace(/\((.+?)\)/, function (m, t) {
				console.info('text in parens: ', arguments, '; script: ', sc);
				if (/[Ѐ-ӿ]/.test(t)) // SOP Russian translation
					return m;
				if (!tr && (sc !== 'Latn')) { // probably a transcription
					tr = t;
					return '';
				} else if (/^[^\[\]]+$/.test(t)) { // probably qualifier
					qual += '; ' + t;
					return '';
				}
				return m; // ???
			});
			
			inp = inp.replace(/\{\{g(\|((?:[mfncsp]|pf|impf)(?:-[mfncsp]|impf|pf)*)*)}}/g, function (m, g) {
				gend = g;
				console.info('gender: ', arguments);
				return '';
			});
			
			if (qual.indexOf('(') !== -1) {
				addError('Opening parenthesis inside qualifier for item "' + t9ns[j] + '". Not touching.');
				allok = false;
				continue;
			}
			if (tr.indexOf('(') !== -1) {
				addError('Opening parenthesis inside transliteration for item "' + t9ns[j] + '". Not touching.');
				allok = false;
				continue;
			}

			var debris = '';
			if (!/^\s*$/.test(inp)) {
				debris = ' ' + inp.trim();
				addError('Warning: some markup with unclear purpose detected: "' + inp + '". Putting after {{t}} invocation.');
				suspc = true;
				susp.push(code);
			}
			
			t9ns[j] = '';
			if (qual !== '')
				t9ns[j] += '{{qualifier|' + qual.substr(2) + '}} ';
			t9ns[j] += '{{t' + ((suspc || (mode === 2)) ? '-check' : '')  + '|' + code + '|' + word;
			t9ns[j] += gend;
			if (tr !== '')
				t9ns[j] += '|tr=' + tr;
			if (alt !== '')
				t9ns[j] += '|alt=' + alt;
			if (sc && (sc !== 'Latn') && (sc !== 'None'))
				t9ns[j] += '|sc=' + sc;
			t9ns[j] += '}}' + debris;
			if (typeof suspc === 'string')
				t9ns[j] += ' {{attention|' + code + '|2=' + suspc + '}}';
			console.info('transformed into ', t9ns[j]);
		}
		if (allok && (subbef === ('{{ttbc|' + code + '}}')))
			subbef = name;
		var newl = (issub ? '*: ' + subbef : ('* ' + subbef)) + ':' + (t9ns.length ? ' ' + t9ns.join(', ') : '');
		if (lines[i] !== newl) {
			lines[i] = newl;
			dirty = true;
		}
	}
	if (!dirty)
		return null;
	return lines.join('\n');
}

function fmtts(ts) {
	function pad(wut) {
		return wut < 10 ? '0' + wut : wut.toString();
	}
	var d = new Date(ts);
	return d.getUTCFullYear().toString() + pad(d.getUTCMonth() + 1) + pad(d.getUTCDate()) + pad(d.getUTCHours()) + pad(d.getUTCMinutes()) + pad(d.getUTCSeconds());
}

function fixer(secnum) {
	return function () {
		if (!code2nameLookup) {
			mw.util.jsMessage('Please wait a moment until language data is downloaded.');
		}
		var formcont, formstart, formtoken, formedit, formwatch, formsumm;
		var form = el('form', [
			formcont = ih('wpTextbox1'),
			formstart = ih('wpStarttime'),
			formedit = ih('wpEdittime'),
			formtoken = ih('wpEdittoken'),
			formwatch = ih('wpWatchthis', '1'),
			ih('oldid', '0'),
			secnum !== null ? ih('wpSection', secnum.toString()) : '',
			formsumm = ih('wpSummary', 'Fixing translation table to use {{t}} ([[User:Kephir/gadgets/xte|assisted]])'),
			ih('wpDiff', 'wpDiff')
		], {
			'action': mw.config.get('wgScript') + '?xtewarn&action=submit' + (secnum !== null ? '&section=' + secnum : '') + '&title=' + encodeURIComponent(wgPageName),
			'method': 'post',
			'enctype': 'multipart/form-data'
		});

		var api = new mw.Api();
		api.get({
			action: 'query',
			prop: 'info|revisions',
			rvprop: 'timestamp|content',
			rvsection: secnum !== null ? secnum : void(0),
			rvlimit: 1,
			rvdir: 'older',
			intoken: 'edit',
			inprop: 'watched',
			titles: wgPageName
		}, {
			'success': function (result) {
				var pid = Object.keys(result.query.pages)[0];
				var pg = result.query.pages[pid];
				var susp = [], errs = [];
				var fixd = fix(pg.revisions[0]['*'], susp, errs);
				if (errs.length) {
					try {
						var cnam = 'kephir-xte-errors-' + wgPageName;
						var cval = JSON.stringify(errs);
						window.sessionStorage.setItem(cnam, cval);
						if (window.sessionStorage.getItem(cnam) !== cval) {
							throw new Error("sessionStorage failed to work");
						}
					} catch (e) {
						alert('Errors while processing:\n\n' + errs.map(function (item) {
							return 'Line ' + item.line + (item.code === null ? '' : ' [' + item.code + ']') + ': ' + item.mesg;
						}).join('\n'));
					}
				}
				if (fixd === null) {
					if (confirm('Nothing fixed automatically (' + errs.length + ' messages). Edit manually?')) {
						location.href = mw.config.get('wgScript') + '?xtewarn&action=edit&title=' + encodeURIComponent(wgPageName) + (secnum !== null ? '&section=' + secnum.toString() : '');
					}
					return;
				}
				if (susp.length) {
					susp.sort();
					var last;
					formsumm.value += ' — please review: ' + susp.filter(function(it) {
						if (it !== last) {
							last = it;
							return true;
						}
						return false;
					}).join(', ');
				}
				formedit.value = fmtts(pg.revisions[0].timestamp);
				formstart.value = fmtts(pg.starttimestamp);
				formtoken.value = pg.edittoken;
				formcont.value = fixd;
				if (!(('watched' in pg) || (mw.user.options.get('watchdefault') == '1'))) {
					formwatch.parentNode.removeChild(formwatch);
				}
				document.body.appendChild(form);
				form.submit();
			},
			'error': function () {
				console.error(arguments);
				alert('Error while requesting page source. See console for details.');
			}
		});
	};
}

var menuList = el('ul');
var menu = el('div', [menuList]);
menu.style.display = 'none';
menu.style.border = '1px solid black';
menu.style.padding = '2px';
menu.style.fontSize = '9pt';
menu.style.background = '#eee';
menu.style.zIndex = 31337;

function addMenuLink(label, title, handler, key) {
	var item;
	menuList.appendChild(el('li', [
		item = el('a', [label], { 'href': 'javascript:void(0);', 'title': title, 'accesskey': key }, {
			'click': function (ev) {
				menu.style.display = 'none';
				handler.apply(this, arguments);
			}
		})
	]));
	return item;
}

function displacement(node) {
	var x = 0, y = 0;
	while (node.offsetParent) {
		x += node.offsetLeft;
		y += node.offsetTop;
		node = node.offsetParent;
	}
	return [x, y];
}

var menuHead = mw.util.addPortletLink(mw.config.get('skin') === 'vector' ? 'p-views' : 'p-cactions',
	'javascript:void(0);', 'xte', 'p-kephir-xte', "the experimental translator's extension", 'o'
);
menu.style.position = 'absolute';
menu.style.zIndex = '666';
document.body.appendChild(menu);
menuHead.addEventListener('click', function (ev) {
	menu.style.display = menu.style.display === '' ? 'none' : '';
	if (menu.style.display !== 'none') {
		var dhead = displacement(menuHead);
		var dpar = displacement(menu.offsetParent);
		menu.style.left = (dhead[0] - dpar[0]) + 'px';
		menu.style.top = (dhead[1] - dpar[1] + menuHead.offsetHeight + 4) + 'px';
	}
}, false);

addMenuLink('Query', 'Ask about language codes or names', function () {
	var query = prompt('What do you want to know about?', 'code or name');
	var t, name, code, sc;
	if (query.toLowerCase() === query) {
		if (t = code2name(query)) {
			code = query;
			name = t;
		} else {
			code = name2code(query);
			name = query;
		}
	} else {
		if (t = name2code(query)) {
			name = query;
			code = t;
		} else {
			if (name = code2name(query))
				code = query;
		}
	}
	if (code) {
		sc = code2sc(code);
		alert('Code: ' + code + '\nLanguage: ' + name + (sc ? '\nTypical script code: ' + sc : ''));
	} else
		alert('Unrecognized query.');
}, '/');

if ((mw.config.get('wgAction') === 'view') && (mw.config.get('wgNamespaceNumber') === 0)) {
	var hls = mw.util.$content[0].getElementsByClassName('mw-headline');
	for (var i = 0; i < hls.length; ++i) {
		if (hls[i].textContent !== 'Translations')
			continue;
		var m, secnum, as = hls[i].parentNode.getElementsByTagName('a');
		for (var j = 0; j < as.length; ++j)
			if (m = /&action=edit&section=(\d+)$/.exec(as[j].href))
				secnum = m[1];
		hls[i].parentNode.appendChild(el('span', [
			'[', el('a', ['fix'], { 'href': 'javascript:void(null);' }, { 'click': fixer(secnum) }), ']'],
			{ 'class': 'mw-editsection' }
		));
	}

	addMenuLink('Fix translations', 'Make translations use {{t}}', function (ev) {
		fixer(null)();
	}, '\\');
}

if (((mw.config.get('wgAction') === 'submit') || (mw.config.get('wgAction') === 'edit'))) {
	var genErrorList = function (errlist) {
		function lineSelector(ln) {
			return function () {
				var start = 0;
				for (var i = 1; i < ln; ++i)
					start = textbox.value.indexOf('\n', start) + 1;
				var end = textbox.value.indexOf('\n', start);

				textbox.focus();
				if (textbox.createTextRange) { // MSIE	
					var range = textbox.createTextRange();
					range.collapse(true);
					range.moveStart('character', start);
					range.moveEnd('character', end);
					range.select();
				} else if (textbox.setSelectionRange) { // browsers
					textbox.setSelectionRange(start, end);
					var phantom = document.createElement('div');
					var style = window.getComputedStyle(textbox, '');
					phantom.style.padding = '0';
					phantom.style.lineHeight = style.lineHeight;
					phantom.style.fontFamily = style.fontFamily;
					phantom.style.fontSize = style.fontSize;
					phantom.style.fontStyle = style.fontStyle;
					phantom.style.fontVariant = style.fontVariant;
					phantom.style.letterSpacing = style.letterSpacing;
					phantom.style.border = style.border;
					phantom.style.outline = style.outline;
					try { phantom.style.whiteSpace = "-moz-pre-wrap" } catch(e) {}
					try { phantom.style.whiteSpace = "-o-pre-wrap" } catch(e) {}
					try { phantom.style.whiteSpace = "-pre-wrap" } catch(e) {}
					try { phantom.style.whiteSpace = "pre-wrap" } catch(e) {}
					phantom.textContent = textbox.value.substr(0, start);
					document.body.appendChild(phantom); // XXX: do I need this?
					textbox.scrollTop = phantom.scrollHeight - (textbox.clientHeight / 2);
					document.body.removeChild(phantom);
				}
				textbox.scrollIntoViewIfNeeded();
			};
		}
		var errul = el('ul');
		var listdom = el('div', [
			el('p', ['Errors and warnings while processing:']),
			errul
		]);
		if (!errlist.length)
			return null;
		for (var i = 0; i < errlist.length; ++i) {
			var linelink = el('a', ['Line ' + errlist[i].line], { 'href': 'javascript:void(0);' });
			linelink.addEventListener('click', lineSelector(errlist[i].line));
			var m, errmsg = [], mesg = errlist[i].mesg;
			while (m = /(.*?)(\[\[(.*?)(\|.*?)?\]\])/.exec(mesg)) {
				errmsg.push(m[1]);
				errmsg.push(el('a', [m[2]], { 'href': mw.util.getUrl(m[3]) }));
				mesg = mesg.substr(m[0].length);
			}
			errmsg.push(mesg);
			errul.appendChild(el('li', [
				linelink, (errlist[i].code ? ' [' + errlist[i].code + ']' : ''), ': '
			].concat(errmsg)));
		}
		return listdom;
	};
	var diff = document.getElementById('wikiDiff');
	var xtebox = document.createElement('div');
	var fishy = false;
	var textbox = document.getElementById('wpTextbox1');
	if (diff) {
		diff.parentNode.insertBefore(xtebox, diff);
		if (/[?&]xtewarn/.test(location.search)) {
			xtebox.appendChild(el('p', [
				el('strong', ["Warning"]), ": the translation fixing mechanism is less than perfect and may generate erroneous markup in some unusual cases. Thoroughly review and correct the diff before saving the page. When unsure what to do, keep the old version of a line and leave a message in the edit summary, or not save at all. You take all responsibility for any changes you save."
			], { 'class': 'warning' }), diff);
			try {
				var errlist = JSON.parse(window.sessionStorage.getItem('kephir-xte-errors-' + wgPageName));
				var eldom = genErrorList(errlist);
				if (eldom)
					xtebox.appendChild(eldom);
				window.sessionStorage.removeItem('kephir-xte-errors-' + wgPageName);
			} catch (e) {
				console.error('failed to parse error list ', e.message, e);
			}
		}
		var restorer = function (linenum, linetext, linechk) {
			return function () {
				if (fishy)
					return;
				var oldline = textbox.value.split(/\n/g)[linenum - 1];
			};
		};

		var rows = diff.getElementsByTagName('tr');
		var curlin = null;
		for (var i = 0; i < rows.length; ++i) {
			var dln = rows[i].getElementsByClassName('diff-lineno');
			if (dln.length === 2) {
				var m = /(\d+):$/.exec(dln[1].textContent);
				curlin = parseInt(m[1], 10);
				continue;
			}
			if (curlin === null)
				continue;
			var dm = rows[i].getElementsByClassName('diff-marker');
			if (!dm.length) {
				fishy = true;
				alert('Something is fishy with the diff.');
				continue;
			}
			if (dm[0].nextElementSibling.className == 'diff-deletedline') {
				var text = dm[0].nextElementSibling.textContent;
				var link = el('a', [], { 'href': 'javascript:void(0);' }, { 'click': restorer(curlin, text) });
				while (dm[0].hasChildNodes()) {
					link.appendChild(dm[0].firstChild);
				}
				dm[0].appendChild(link);
			}
			++curlin;
		}
	}

	addMenuLink('Fix translations', 'Make translations use {{t}}', function () {
		var susp = [], errors = [];
		var fixed = fix(textbox.value, susp, errors);
		if (fixed === null) {
			alert('No fixes made. Errors while processing:\n\n' + errors.map(function (item) {
				return 'Line ' + item.line + (item.code === null ? '' : ' [' + item.code + ']') + ': ' + item.mesg;
			}).join('\n'));
			return;
		}
		while (xtebox.firstChild)
			xtebox.removeChild(xtebox.firstChild);
		xtebox.appendChild(el('p', [
			el('strong', ["Warning"]), ": the translation fixing mechanism is less than perfect and may generate erroneous markup in some unusual cases. Thoroughly review and correct the diff before saving the page. When unsure what to do, keep the old version of a line and leave a message in the edit summary, or not save at all. You take all responsibility for any changes you save."
		], { 'class': 'warning' }));
		var eldom = genErrorList(errors);
		if (eldom)
			xtebox.appendChild(eldom);
		textbox.value = fixed;
	}, '\\');
}

(function () {

var basket;

function getBasket() {
	basket = JSON.parse(localStorage.getItem('kephir-xte-basket'));
}

function saveBasket() {
	localStorage.setItem('kephir-xte-basket', JSON.stringify(basket));
}

try {
	if (!window.localStorage)
		throw new Error("localStorage is absent");
	if (!window.localStorage.getItem('kephir-xte-basket')) {
		window.localStorage.setItem('kephir-xte-basket', '[]');
		if (window.localStorage.getItem('kephir-xte-basket') !== '[]') {
			throw new Error("localStorage does not work");
		}
	}
} catch (e) {
	return;
}

getBasket();

addMenuLink('Show basket', 'Show basket contents', function () {
	getBasket();
	mw.util.jsMessage('<p>Contents of the basket (<b>'+ basket.length +'</b> items):</p><ul style="max-height: 15em; overflow-y: auto; margin: 0; padding: 0 0 0 1em;">' + basket.map(function(item) { return '<li><a href="' + mw.util.getUrl(item).replace(/[&"]/g, function (m) { return '&#' + m.charCodeAt(0) + ';' }) + '">' + item + '</a></li>'; }).join('') + '</ul>');
});

addMenuLink('Clear basket', 'Remove all pages from the basket', function () {
	basket = [];
	saveBasket();
	mw.util.jsMessage('Basket cleared');
});

addMenuLink('Add to/remove this page', 'Add this page to the basket', function () {
	getBasket();
	var ind = basket.indexOf(wgPageName);
	if (ind === -1) {
		basket.push(wgPageName);
		mw.util.jsMessage('Added <b>' + wgPageName + '</b> to the basket.');
	} else {
		basket.splice(ind, 1);
		mw.util.jsMessage('Removed <b>' + wgPageName + '</b> from the basket.');
	}
	saveBasket();
});

if (mw.config.get('wgAction') === 'view')
addMenuLink('Bubuh pautan ke dalam laman ini', 'Tambah semua pautan wiki dari halaman ini ke ruang nama utama ke dalam bakul', function () {
	getBasket();
	var links = document.getElementById('mw-content-text').getElementsByTagName('a');
	var added = 0, m;
	for (var j = 0; j < links.length; ++j) {
		var u = new mw.Uri(links[j].href);
		if (u.host !== location.host)
			continue;
		if (m = /^\/wiki\/([^?#{}|[\]]+)$/.exec(u.path)) {
			var t = new mw.Title(decodeURIComponent(m[1]));
			if (t.getNamespaceId() !== 0) continue;
			basket.push(t.getPrefixedDb());
			added++;
		}
	}
	saveBasket();
	mw.util.jsMessage('Laman <b>' + added + '</b> ditambah.');
});

if (((mw.config.get('wgAction') === 'submit') || (mw.config.get('wgAction') === 'edit'))) {

addMenuLink('Bubuh pautan ke dalam penyunting', 'Tambah semua (secara terus) pautan wiki kepada ruang nama utama dalam editor kepada bakul', function () {
	getBasket();
	var editor = document.getElementById('wpTextbox1');
	var added = 0;
	editor.value.replace(/\[\[([^#|{}[\]]*?)(?:]]|\|)/g, function (m, target) {
		var t = new mw.Title(target);
		if (t.ns !== 0) return;
		basket.push(t.getPrefixedDb());
		added++;
	});
	saveBasket();
	mw.util.jsMessage('Laman <b>' + added + '</b> ditambah.');
});

addMenuLink('Bubuh pautan ke dalam penyunting', 'Lampirkan senarai halaman dalam bakul kepada editor', function () {
	getBasket();
	var editor = document.getElementById('wpTextbox1');
	if (editor.value && editor.value.substr(-1) !== '\n')
		editor.value += '\n';
	for (var j = 0; j < basket.length; ++j) {
		editor.value += '* [[' + basket[j] + ']]\n';
	}
});

}

addMenuLink('Pilih laman rawak', 'Pilih halaman rawak dari bakul ini untuk memulakan', function (ev) {
	getBasket();
	var j = Math.floor(Math.random() * basket.length);
	var page = basket.splice(j, 1);
	saveBasket();
	location.href = mw.util.getUrl(page[0]);
}, ']');

})();

addMenuLink('Maklum balas', 'Hantarkan maklum balas kepada pengimport', function () {
	location.href = mw.config.get('wgScript') + '?title=Perbincangan_pengguna:Kephir/gadgets/xte&action=edit&section=new&preloadtitle=Awesome+script!';
});

});