Jump to content

User:Kephir/gadgets/unclutter.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Kephir (talk | contribs) at 07:52, 7 November 2014 (tpyo). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// more info: [[User:Kephir/gadgets/unclutter]]
/*jshint shadow:true, latedef:true, boss:true, scripturl:true, loopfunc:true, undef:true */
/*global mw, jQuery, importStylesheet */
mw.loader.using(['mediawiki.user', 'mediawiki.util', 'mediawiki.Title', 'mediawiki.Uri'], function() {
"use strict";

var wgConf = mw.config.get();

var settings = window.kephirUnclutter || {};
var defSettings = {
	enableOnPageLoad            : true,
	wrapDiscussions             : true,
	collapseDiscussionsByDefault: false,
	signaturesProcess           : true,
	signaturesMinimise          : true,
	signaturesColourise         : true,
	signaturesExceptions        : [],
	liquidThreadsColourPosts    : true,
	userAnnotations             : {},
	processSig                  : function (context) { return !settings.signaturesMinimise; },
	postProcessSig              : function (context) { return false; },
	discussionPagePatterns      : []
};

if (wgConf.wgServer === '//en.wikipedia.org') {
	// workaround for [[User:Writ Keeper/Scripts/teahouseTalkbackLink.js]]
	if (window.talkbackSubmit && window.teahouseTalkbackLink && (wgConf.wgPageName === 'Wikipedia:Teahouse/Questions')) {
		defSettings.signaturesMinimise = false;
		// defSettings.postProcessSig
	}

	defSettings.userAnnotations = {
		"קיפודנחש"              : 'kipod',
		"הסרפד"                 : 'Hasirpad',
		"Eric Corbett"          : 'Malleus Fatuorum',
		"㓟"                     : 'Pi',
		"とある白い猫"             : 'White Cat',
		"You Can Act Like A Man": "Basket Feudalist",
		"Piotrus"               : "Piotr Konieczny",
		"Thumperward"           : "Chris Cunningham",
		"Pigsonthewing"         : "Andy Mabbett",
		"Underlying lk"         : "eh bien mon prince",
		"QTxVi4bEMRbrNqOorWBV"  : "jps",
		"Hisashiyarouin"        : "野狼院ひさし",

		"": ""
	};

	defSettings.discussionPagePatterns = [
		/^Wikipedia:Administrators'_noticeboard\//,
		/^Wikipedia:Articles_for_deletion\//,
		/^Wikipedia:Templates_for_discussion/,
		/^Wikipedia:Possibly_unfree_files/,
		/^Wikipedia:Files_for_deletion/,
		/^Wikipedia:Redirects_for_discussion/,
		/^Wikipedia:Categories_for_discussion/,
		/^Wikipedia:Miscellany_for_deletion/,
		/^Wikipedia:Sockpuppet_investigations\//,
		/^Wikipedia:Teahouse\/Questions/,
		/^Wikipedia:Deletion_review\//
	];
}

if (wgConf.wgServer === '//en.wiktionary.org') {
	defSettings.discussionPagePatterns = [
		/^Wiktionary:Votes(\/|$)/,
		/^Wiktionary:Beer_parlour(\/|$)/,
		/^Wiktionary:Grease_pit(\/|$)/,
		/^Wiktionary:Etymology_scriptorium(\/|$)/,
		/^Wiktionary:Information_desk(\/|$)/,
		/^Wiktionary:Tea_room(\/|$)/,
	];
}

for (var key in defSettings) {
	if (!(key in settings))
		settings[key] = defSettings[key];
}
var sheet;

function checkSheet(sheet) {
	var ret, m;
	if (!sheet);
		return null;

	var uri = new mw.Uri(sheet.href);

	if ((uri.host === location.hostname) && (uri.path === wgConf.wgScript)) {
		if ((uri.query.action === 'raw') && (uri.query.title === 'User:Kephir/gadgets/unclutter.css'))
			return sheet;
	}

	if (sheet.rules) {
		for (var j = 0; j < sheet.rules.length; ++j) {
			if (ret = checkSheet(sheet.rules[j].styleSheet))
				return ret;
		}
	}
	return null;
}

for (var i = 0; i < document.styleSheets.length; ++i) {
	if (sheet = checkSheet(document.styleSheets[i]))
		break;
}

if (!sheet)
	sheet = importStylesheet('User:Kephir/gadgets/unclutter.css');

var sheetToggler = mw.util.addPortletLink('p-tb', 'javascript:void(0);', 'Toggle Unclutter', 'p-kephir-unclutter');
sheetToggler.addEventListener('click', function (ev) {
	ev.preventDefault();
	sheet.disabled = !sheet.disabled;
}, false);

sheet.disabled = !settings.enableOnPageLoad;

var isDiscussionPage = settings.isDiscussionPage;

if (wgConf.wgServer === '//en.wikipedia.org') {
	(function (grps) {
		if (grps.indexOf('autoconfirmed') !== -1)
			return; // sorry to bother you.

		if (settings.enableOnPageLoad) {
			sheet.disabled = true;
			alert('You have installed Unclutter, but are not autoconfirmed. This means that you are probably an inexperienced editor. Unclutter usage by inexperienced editors is discouraged, as the stylesheet hides advice which new editors might need to be reminded of; therefore the stylesheet has been disabled.\n\nThis message will display on every page load until you configure Unclutter to disable the stylesheet by default. Read [[User:Kephir/gadgets/unclutter#Configuration]] to learn how to do it.');
		}
	})(wgConf.wgUserGroups);
}

if (isDiscussionPage === void(0)) {
	isDiscussionPage = (((wgConf.wgNamespaceNumber >= 0) && (wgConf.wgNamespaceNumber % 2)) || document.getElementById('ca-addsection'));

	if (!isDiscussionPage) for (var i = 0; i < settings.discussionPagePatterns.length; ++i) {
		if (settings.discussionPagePatterns[i].test(wgConf.wgPageName)) {
			isDiscussionPage = true;
			break;
		}
	}

	if (wgConf.wgServer === '//en.wikipedia.org') {
		if (/^Wikipedia_talk:Articles_for_creation\//i.test(wgConf.wgPageName))
			isDiscussionPage = false;
	}

	if (window.liquidThreads)
		isDiscussionPage = false; // XXX: for the purpose of section collapsing, that is.
}

function link(handler, text) {
	var span = document.createElement('span');
	var a = document.createElement('a');
	var tx = document.createTextNode(text);
	a.appendChild(tx);
	a.href = '#';
	a.addEventListener('click', function (ev) {
		ev.preventDefault();
		ev.stopPropagation();
		return handler(ev, a, tx, span);
	}, false);
	var lb = document.createElement('span');
	var rb = document.createElement('span');
	lb.textContent = '['; lb.className = 'mw-editsection-bracket';
	rb.textContent = ']'; rb.className = 'mw-editsection-bracket';
	span.appendChild(lb);
	span.appendChild(a);
	span.appendChild(rb);
	span.click = function () {
		a.click.apply(a, arguments);
	};
	return span;
}

var togglerStore = {};
try {
	togglerStore = JSON.parse(window.localStorage.getItem('kephir-unclutter-togglers')) || {};
} catch (e) {
	/* swallow */
}

function toggler(element, state, stateKey, showText, hideText) {
	showText = showText || 'show';
	hideText = hideText || 'hide';
	var tstore;
	state == stateKey in togglerStore ? togglerStore[stateKey] : state;
	element.style.display = state ? 'none' : '';
	return link(function (ev, a, tx, span) {
		state = !state;
		element.style.display = state ? 'none' : '';
		tx.data = state ? showText : hideText;
		if (stateKey) {
			try {
				togglerStore = JSON.parse(window.localStorage.getItem('kephir-unclutter-togglers')) || {};
			} catch (e) {
				/* swallow */
			}
			togglerStore[stateKey] = state;
			try {
				window.localStorage.setItem('kephir-unclutter-togglers', JSON.stringify(togglerStore));
			} catch (e) {
				/* swallow */
			}
		}
		return false;
	}, state ? showText : hideText);
}

function ancestors(node) {
	var result = [];
	while (node !== null) {
		result[result.length] = node;
		node = node.parentNode;
	}
	return result;
}

function clicker(target) {
	return function (ev) {
		if (ancestors(ev.target).filter(function (item) { return item.tagName === 'A'; }).length) {
			return;
		}
		target.click();
	};
}

function processPreview() {
	var tu = document.getElementsByClassName('templatesUsed')[0], tup, tuul;
	var pt;
	if (tu) {
		tup = tu.getElementsByTagName('p')[0];
		tuul = tu.getElementsByTagName('ul')[0];
	}

	var hc = document.getElementsByClassName('hiddencats')[0], hcp, hcul;
	if (hc) {
		hcp = hc.getElementsByTagName('p')[0];
		hcul = hc.getElementsByTagName('ul')[0];
	}

	var ph = document.getElementById('mw-previewheader');
	if (tup && tuul) {
		tup.appendChild(document.createTextNode(' (' + tuul.getElementsByTagName('li').length + ')'));
		tup.appendChild(pt = toggler(tuul, true));
		pt.className = 'mw-editsection';
		tuul.style.display = 'none';
	}

	if (hcp && hcul) {
		// hcp.appendChild(document.createTextNode(' (' + hcul.getElementsByTagName('li').length + ')'));
		hcp.appendChild(pt = toggler(hcul, true));
		pt.className = 'mw-editsection';
		hcul.style.display = 'none';
	}

	if (ph) {
		var pc = document.getElementById('wikiPreview').getElementsByClassName('mw-content-ltr')[0]; // XXX
		ph.appendChild(pt = toggler(pc, false));
		pt.className = 'mw-editsection';
	}

	/* TODO: handle #wikiDiff too */
}

if ((wgConf.wgAction === 'edit') || (wgConf.wgAction === 'submit')) {
	processPreview();
	if (mw.user.options.get('uselivepreview') == '1') {
		jQuery(mw).bind('LivePreviewDone', processPreview);
	}
}

var enarea = document.getElementById('editnotice-area');
if (((wgConf.wgAction === 'submit') || (wgConf.wgAction === 'edit')) && enarea) {
	var enn = enarea.getElementsByClassName('editnotice-namespace')[0];
	var nonempty = enn && enn.textContent.trim();
	var editintro = document.getElementsByClassName('mw-editintro')[0];
	if (!nonempty) {
		var enl = enarea.getElementsByClassName('editnotice-link')[0];
		nonempty = enl && !enl.classList.contains("editnotice-redlink");
	}

	if (editintro) {
		var einame = mw.util.getParamValue('editintro');

		var eihider = document.createElement('div');
		eihider.className = 'kephir-unclutter-editintro-hide-link';
		eihider.style.fontSize = 'smaller';
		eihider.style.textAlign = 'right';
		eihider.appendChild(toggler(editintro, false, einame ? ('editintro-' + einame) : null, 'show editintro', 'hide editintro'));
		editintro.parentNode.insertBefore(eihider, editintro);
	}

	if (nonempty) {
		var hider = document.createElement('div');
		hider.style.fontSize = 'smaller';
		hider.style.textAlign = 'right';
		hider.className = 'kephir-unclutter-editnotice-hide-link';
		hider.appendChild(toggler(enarea, false, 'editnotice-' + wgConf.wgPageName, 'show editnotices', 'hide editnotices'));
		enarea.parentNode.insertBefore(hider, enarea);
	}
}

if (wgConf.wgAction === 'submit') {
	var ph = document.getElementById('mw-previewheader');
	if (ph) {
		var cont = document.getElementById('wikiPreview').getElementsByClassName('mw-content-ltr')[0]; // XXX
		ph.insertBefore(toggler(cont, false), ph.firstChild);
	}
}

if (wgConf.wgAction === 'view') {
	var content = document.getElementById('mw-content-text');
	var toc = document.getElementById('toc');
	var allHidden = settings.collapseDiscussionsByDefault;
	var headlines = document.getElementsByClassName('mw-headline');
	if ((headlines.length > 1) && settings.wrapDiscussions && isDiscussionPage) {
		var wrappers = {};
		var sect0wrapper = document.createElement('div');
		var l0, l1;
		content.insertBefore(sect0wrapper, content.firstChild);
		while (sect0wrapper.nextSibling) {
			if (sect0wrapper.nextSibling.contains(headlines[0]))
				break;
			sect0wrapper.appendChild(sect0wrapper.nextSibling);
		}
		sect0wrapper.className = 'kephir-unclutter-sectionzero-wrapper';
		document.getElementById('siteSub').appendChild(l0 = toggler(sect0wrapper, false, 'sect0-' + wgConf.wgPageName, 'show header', 'hide header'));
		document.getElementById('siteSub').appendChild(l1 = link(function(ev, a, tx, span) {
			allHidden = !allHidden;
			tx.data = allHidden ? 'show topics': 'hide topics';
			for (var key in wrappers) {
				if (wrappers[key].wrapper.style.display !== (allHidden ? 'none' : ''))
					wrappers[key].click();
			}
		}, allHidden ? 'show topics' : 'hide topics'));
		l0.className = l1.className = 'mw-editsection';
		for (var i = 0; i < headlines.length; ++i) {
			var hdr = headlines[i].parentNode;
			var nhdr = null;
			for (var j = i + 1; j < headlines.length; ++j) {
				if (headlines[j].parentNode.tagName === hdr.tagName) {
					nhdr = headlines[j].parentNode;
					break;
				}
			}
			var wrapper = document.createElement('div');
			hdr.parentNode.insertBefore(wrapper, hdr.nextSibling);
			while (wrapper.nextSibling) {
				if (wrapper.nextSibling.contains(nhdr))
					break;
				wrapper.appendChild(wrapper.nextSibling);
			}
			wrapper.className = 'kephir-unclutter-' + hdr.tagName + '-contents kephir-unclutter-discussion-contents';
			var headerLink = toggler(wrapper, (hdr.tagName === 'H2') && settings.collapseDiscussionsByDefault, 'header-' + wgConf.wgPageName + '#' + headlines[i].id);
			var superwrapper = document.createElement('div');
			hdr.parentNode.insertBefore(superwrapper, hdr);
			superwrapper.appendChild(hdr);
			superwrapper.appendChild(wrapper);
			superwrapper.className = 'kephir-unclutter-' + hdr.tagName + '-wrapper kephir-unclutter-discussion-wrapper';
			headerLink.className = 'mw-editsection';
			headerLink.wrapper = wrapper;
			hdr.appendChild(headerLink);
			hdr.style.cursor = 'pointer';
			hdr.classList.add('kephir-unclutter-collapsible-section');
			hdr.addEventListener('click', clicker(headerLink), false);
			wrappers[headlines[i].id] = headerLink;
		}
		var oldhash = location.hash.substr(1);
		location.hash = '';
		window.addEventListener('hashchange', function (ev) {
			var id = location.hash.substr(1);
			if (wrappers[id]) {
				var h = document.getElementById(id);
				if (wrappers[id].wrapper.style.display === 'none') {
					wrappers[id].click();
					window.setTimeout(function() {
						h.scrollIntoView();
					}, 0); // fix scrolling position
				}
			}
		}, false);
		location.hash = oldhash;
	}
}

function escapeClass(clbutt) {
	return clbutt.replace(/[^_a-zA-Z0-9-]/g, function (m) {
		/* TODO: do I care or do I not about non-BMP usernames? */
		var t = m.charCodeAt(0).toString(16);
		while (t.length < 6)
			t = '0' + t;
		return '\\' + t;
	});
}

var redirCache = {};

function solveRedirect(title) {
	if (title in redirCache)
		return redirCache[title];
	var api = new mw.Api();
	var result = redirCache[title] = title;
	api.ajax({
		action: 'query',
		titles: title,
		redirects: '1'
	}, {
		async: false,
		success: function (data) {
			result = redirCache[title] = data.query.redirects[0].to;
		},
		error: function () {
			throw new Error('redirect resolution failed');
		}
	});
	return result;
}

function normaliseUsername(uname) {
	var tit;
	try {
		tit = new mw.Title('User:' + uname);
	} catch (e) {
		return null;
	}
	var norm = tit.getMainText();
	return norm;
}

function unameClasses(username) {
	if (mw.util.isIPv4Address(username))
		return 'unregistered ipv4';
	if (mw.util.isIPv6Address(username))
		return 'unregistered ipv6';
	return '';
}

var rxPage = new RegExp('^' + wgConf.wgArticlePath.replace('$1', '(.*)') + '$');
function targetPage(href, params) {
	var uri;
	try {
		uri = new mw.Uri(href);
	} catch (e) {
		return null;
	}

	if ((uri.host !== location.hostname))
		return null;
	if (params)
		for (var k in uri.query)
			params[k] = uri.query[k];

	try {
		if (m = rxPage.exec(uri.path))
			return new mw.Title(decodeURIComponent(m[1]) + (uri.fragment ? '#' + uri.fragment : '')); // XXX: why does mw.Title not decodeURIComponent on its own?
		else if (uri.path === wgConf.wgScript)
			return new mw.Title(uri.query.title + (uri.fragment ? '#' + uri.fragment : ''));
	} catch (e) { /* failed to parse, bail out. */ }

	return null;
}

function wrapSignature(node, uname) {
	var badtags = ['DIV', 'DL', 'DD', 'BLOCKQUOTE', 'LI', 'P', 'BR', 'TD', 'TH', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE', 'DEL', 'INS'];
	var wraptags = ['FONT', 'EM', 'B', 'I', 'SMALL'];
	function isStylingNode(node, relax) {
		if (node.tagName === 'FONT')
			return true;
		else if (badtags.indexOf(node.tagName) !== -1)
			return false;
		else if (['SPAN', 'SMALL'].indexOf(node.tagName) === -1)
			if ((['B', 'I', 'EM', 'STRONG'].indexOf(node.tagName) !== -1)
			&& (node.attributes.length === 1) && node.hasAttribute('style'));
				// do the loop check
			else if (!relax || (['B', 'I', 'EM', 'STRONG'].indexOf(node.tagName) === -1)
			|| !((node.attributes.length < 2) && ((node.attributes.length === 1) <= node.hasAttribute('style')))) {
				if (node.childNodes) {
					var hads = false;
					for (var i = 0; i < node.childNodes.length; ++i) {
						if (node.childNodes[i].nodeType !== document.TEXT_NODE && !(hads = isStylingNode(node.childNodes[i], true)))
							return false;
					}
					return hads;
				}
				return false;
			} else;
		else if (!((node.attributes.length === 1) && node.hasAttribute('style')))
			return false;

		if (node.childNodes) {
			for (var i = 0; i < node.childNodes.length; ++i) {
				if (node.childNodes[i].nodeType !== document.TEXT_NODE && !isStylingNode(node.childNodes[i], true))
					return false;
			}
		}
		return true;
	}
	function grabProlog(last, wrapper) {
		var m;
		while (last.previousSibling) {
			if (isStylingNode(last.previousSibling)) {
				wrapper.insertBefore(last.previousSibling, wrapper.firstChild);
				continue;
			}

			if ((last.previousSibling.nodeType === document.TEXT_NODE)
			&& (m = /[\u2000-\u200a\u2011\x20\x09\xa0\x0a\x0d]*([\u2003\x20\x09\xa0\x0a\x0d][([{]|[‑-][‑-]?|[–—~]\/?|·{1,3}|(?:[\x20\xa0]?[᛭•§|\u2190-\u21ff\u2600-\u27bf\u2200-\u22ff])+|[\u2003\x20\x09\xa0\x0a\x0d])[\u2003\x20\x09\xa0\x0a\x0d]*$/.exec(last.previousSibling.data))) {
				last.previousSibling.data = last.previousSibling.data.substr(0, last.previousSibling.data.length - m[0].length);
				wrapper.insertBefore(document.createTextNode(m[0]), wrapper.firstChild);
				if (last.previousSibling.data === '') {
					last.parentNode.removeChild(last.previousSibling);
					continue;
				}
			}
			break;
		}
	}
	function canBeSigNode(subcheck) {
		function checkTarget(tit) {
			if (tit.namespace === wgConf.wgNamespaceIds.special) {
				if (m = /^(Contributions|EmailUser|Log(?:\/delete|\/protect|\/block|))\/(.*)/.exec(tit.getMainText())) {
					if (m[1] === 'EmailUser')
						m[2] = m[2].replace(/^User:/i, '');
					if (normaliseUsername(m[2]) !== uname)
						return 0;
				} else
					return -1;
			} else if ((tit.namespace === wgConf.wgNamespaceIds.user) || (tit.namespace === wgConf.wgNamespaceIds.user_talk)) {
				if (tit.namespace === wgConf.wgNamespaceIds.user_talk)
					if (sigll[href])
						return -2; // a signature never links to the same page twice.
				if ((tit.namespace === wgConf.wgNamespaceIds.user) && (tit.getMainText().substr(0, uname.length + 1) === (uname + '/')))
					return 1;
				if (tit.getMainText() !== uname)
					return 0;
				sigll[href] = true;
				if (!tit.fragment || /^(no ?bold|top)$/i.test(tit.fragment))
					return 1; // OK
				else
					return -3; // invalid!
			}

			if (href.fragment) // section links are probably not parts of a signature.
				return -3;

			return 1;
		}
		if (subcheck.nodeType === document.TEXT_NODE)
			return subcheck.data.length < 64;
		if (subcheck.getElementsByTagName('img').length > 0)
			return null; // no images are allowed in signatures; reject.
		var siglinks = Array.prototype.slice.call(subcheck.getElementsByTagName('a'));
		if (subcheck.tagName === 'A')
			siglinks.push(subcheck);
		if (wgConf.wgServer === '//en.wikipedia.org') {
			// helps with some false positives
			if (subcheck.querySelector('span.plainlinks'))
				return null;
		}
		for (var i = 0; i < siglinks.length; ++i) {
			var href = new mw.Uri(siglinks[i].href);
			var pagepat = new RegExp('^' + wgConf.wgArticlePath.replace(/([\\{}()|.?*+\-\^$\[\]])/g, '\\$1').replace('\\$1', '(.+)') + '$'); // XXX: awful
			var m, tit;

			// workaround for [[User:Writ Keeper/Scripts/teahouseTalkbackLink.js]]
			if (window.teahouseTalkbackLink && /^TBsubmit\d+$/.test(siglinks[i].id))
				continue;

			if (href.host !== wgConf.wgServer.replace(/^\/\//, ''))
				return null; // reject, external link.

			if (m = pagepat.exec(href.path)) {
				tit = new mw.Title(decodeURIComponent(m[1]));
			} else if (href.path === wgConf.wgScript) {
				if (!href.query.title)
					return null;
				tit = new mw.Title(href.query.title);
				if (href.query.action && (['edit', 'view'].indexOf(href.query.action) === -1))
					return null;
				if (href.query.section && (href.query.section !== 'new'))
					return null;
				if (Object.keys(href.query).some(function (item) {
					return ['action', 'section', 'redlink', 'preload', 'title'].indexOf(item) === -1;
				}))
					return null;
			} else
				return null; // looks like an external link. there are not allowed, so no signature here.

			if (!tit)
				return null; // not a good link.

			var newtit = tit;
			if ([
				wgConf.wgNamespaceIds.draft,
				wgConf.wgNamespaceIds.draft_talk,
				wgConf.wgNamespaceIds.help,
				wgConf.wgNamespaceIds.help_talk,
				wgConf.wgNamespaceIds.category,
				wgConf.wgNamespaceIds.category_talk,
				wgConf.wgNamespaceIds.file,
				wgConf.wgNamespaceIds.file_talk,
				wgConf.wgNamespaceIds.mediawiki,
				wgConf.wgNamespaceIds.mediawiki_talk,
				wgConf.wgNamespaceIds.template_talk,
				wgConf.wgNamespaceIds.module,
				wgConf.wgNamespaceIds.module_talk,
			].indexOf(tit.namespace) !== -1)
				return null;

			var why = checkTarget(tit);
			if (why === 0) {
				if (siglinks[i].classList.contains('mw-redirect')) {
					try {
						tit = new mw.Title(solveRedirect(tit.toString()));
					} catch (e) {
						continue; // assume OK
					}
					if (checkTarget(tit) > 0)
						continue;
				}
				return null;
			} else if (why < 0) {
				return null;
			}
		}
		return true;
	}
	var m;
	var initial = node;
	var last = node;
	var wrapper = document.createElement('span');
	// lifted from CiLT
	var tsregex = /(\s*\d?\d:\d\d(\s+[APap](\. ?)?[Mm](\. ?)?)?,?\s+(\d\d?\s+\w\w+|\w\w+\s+\d\d?,?)\s+\d\d\d\d|\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d)(\s*\(UTC\))?/;
	wrapper.className = 'kephir-unclutter-signature-wrapper signature signature-' + uname.replace(/ /g, '_') + ' ' + unameClasses(uname);
	while (last = node, node = node.parentNode) {
		var got = null;
		var sigll = {};
		var totlen = 0;
		var scnm;

		// XXX: handle {{spa}} and such somehow
		/*if (node.tagName === 'SMALL')
			return null;*/
		if (badtags.indexOf(last.tagName) !== -1)
			return null;

		var subcheck = last;
		do {
			if (subcheck.classList && subcheck.classList.contains('before-localcomments')) {
				got = subcheck;
				m = [];
				break;
			} else if (subcheck.getElementsByClassName && (scnm = subcheck.getElementsByClassName('before-localcomments')).length) {
				if (!canBeSigNode(subcheck))
					return null;
				scnm = scnm[0];
				var sceit = scnm, scenext = scnm, posnode = subcheck.nextSibling;
				while (sceit !== subcheck.nextSibling) {
					while (!scenext.nextSibling) {
						scenext = scenext.parentNode;
						if (scenext === subcheck)
							break;
					}
					scenext = scenext.nextSibling;
					subcheck.parentNode.insertBefore(sceit, posnode);
					posnode = sceit.nextSibling;
					sceit = scenext;
				}
				got = subcheck;
				break;
			} else if ((subcheck.nodeType === document.TEXT_NODE) && (m = tsregex.exec(subcheck.data))) {
				// split subcheck into two text nodes: before and after start of match.
				subcheck.parentNode.insertBefore(
					document.createTextNode(subcheck.data.substr(0, m.index)),
					subcheck
				);
				subcheck.data = subcheck.data.substr(m.index);
				if (!canBeSigNode(subcheck.previousSibling))
					return null;
				got = subcheck;
				break;
			} else if (m = tsregex.exec(subcheck.textContent)) {
				if (!canBeSigNode(subcheck))
					return null;

				// dig up the node
				got = subcheck;
				while (got.nodeType !== document.TEXT_NODE) {
					got = got.firstChild;
					while (String(got.textContent).indexOf(m[0]) === -1) {
						got = got.nextSibling;
						if (!got)
							return null; // whoops...
					}
				}

				// split at match
				got.parentNode.insertBefore(
					document.createTextNode(got.data.substr(0, got.data.indexOf(m[0]))),
					got
				);
				got.data = got.data.substr(got.data.indexOf(m[0]));

				// carve it out
				if (got !== subcheck) {
					var sceit = got, scenext = got, posnode = subcheck.nextSibling;
					while (sceit) {
						while (!scenext.nextSibling) {
							scenext = scenext.parentNode;
							if (scenext === subcheck) {
								scenext = null;
								break;
							}
						}
						scenext = scenext && scenext.nextSibling;
						subcheck.parentNode.insertBefore(sceit, posnode);
						posnode = sceit.nextSibling;
						sceit = scenext;
					}
				}
				break;
			} else if ((subcheck.nodeType === document.TEXT_NODE) && (subcheck.data.length >= 64)) {
				// long passages of text? probably a false positive.
				return null;
			}
			if (!subcheck.getElementsByTagName)
				continue;
			if (!canBeSigNode(subcheck))
				return null;
		} while (subcheck = subcheck.nextSibling);

		if (got) {
			grabProlog(last, wrapper);
			if (last.parentNode.firstChild === last) {
				if (wraptags.indexOf(last.parentNode.tagName) !== -1) {
					if (got.nodeType === document.TEXT_NODE) { // raw timestamp
						got.parentNode.parentNode.insertBefore(got, got.parentNode.nextSibling);
						last = last.parentNode;
						node = node.parentNode;
					} else { // LocalComments
						var ggot = got;
						while (ggot && !ggot.classList.contains('after-localcomments')) {
							ggot = ggot.nextSibling;
						}
						if (!ggot || (ggot.parentNode.lastChild === ggot)) {
							var itn = null, lip = got.parentNode.nextSibling;
							for (var it = got; it && it !== ggot.nextSibling; it = itn) {
								itn = it.nextSibling;
								if (lip)
									it.parentNode.parentNode.insertBefore(it, lip);
								else
									it.parentNode.parentNode.appendChild(it);
								lip = it.nextSibling;
							}
							last = last.parentNode;
							node = node.parentNode;
						}
					}
				}
			}
			grabProlog(last, wrapper);
			node.insertBefore(wrapper, last);

			for (var it = last; it && it !== got; it = wrapper.nextSibling) {
				wrapper.appendChild(it);
			}
			if (got.nodeType !== document.TEXT_NODE) {
				wrapper.appendChild(got);
			}

			return wrapper;
		}
	}
	return null;
}

// XXX: customising colours? remembering across pages?
var userColours, userLQTColours;
var userColoursQueue = { };
var userColoursSheet;

try {
	userColours = JSON.parse(window.sessionStorage.getItem('kephir-unclutter-sigcolours-' + wgConf.wgPageName)) || {};
	userLQTColours = JSON.parse(window.sessionStorage.getItem('kephir-unclutter-lqtcolours-' + wgConf.wgPageName)) || {};
} catch (e) {
	/* ignore */
}

if (settings.signaturesColourise) {
	var styleElm = document.createElement('style');
	styleElm.appendChild(document.createTextNode(''));
	document.head.appendChild(styleElm);
	userColoursSheet = styleElm.sheet || styleElm.styleSheet;
	if (!userColoursSheet) {
		settings.signaturesColourise = false;
	}
	if (!userColoursSheet.addRule) {
		if (userColoursSheet.insertRule)
			userColoursSheet.addRule = function (selector, css) {
				this.insertRule(selector + '{' + css + '}', 0);
			};
		else {
			settings.signaturesColourise = false;
			userColoursSheet = null;
		}
	}
}

if (userColoursSheet)
for (var uname in userColours) {
	var colour = userColours[uname];
	var lqtc = userLQTColours[uname];
	userColoursSheet.addRule(
		'.kephir-unclutter-minisig.signature-' + escapeClass(uname.replace(/ /g, '_')),
		'background: ' + colour
	);
	if (settings.liquidThreadsColourPosts && lqtc) {
		userColoursSheet.addRule(
			'.lqt-post-wrapper.post-' + escapeClass(uname.replace(/ /g, '_')),
			'background: ' + lqtc
		);
	}
}

function pickSigColour() {
	// XXX: minimise collisions
	var hue = Math.random() * 360;
	var sat = Math.sqrt(Math.random()) * 100;
	var lum = 50 + Math.pow(Math.random(), 1/3) * 30;

	return {
		lqtVariant: {
			hue: hue, sat: sat / 2, lum: 90,
			toString: function () {
				return 'hsl(' + this.hue + ',' + this.sat + '%, ' + this.lum + '%)';
			}
		},
		hue: hue, sat: sat, lum: lum,
		toString: function () {
			return 'hsla(' + this.hue + ',' + this.sat + '%, ' + this.lum + '%, 0.175)';
		}
	};
}

function minimiseSig(wrapper, username, annotation) {
	function displacement(node) {
		var x = 0, y = 0;
		while (node.offsetParent) {
			x += node.offsetLeft;
			y += node.offsetTop;
			node = node.offsetParent;
		}
		return [x, y];
	}
	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 attr === 'string') {
			attr = { title: attr };
		}
		if (typeof href === 'string')
			attr.href = href;
		else {
			attr.href = 'javascript:void(null);';
			ev.click = href;
		}
		return el('a', child, attr, ev);
	}
	function cwrap(clbutt, child) {
		return el('span', child, { 'class': clbutt });
	}
	if (settings.signaturesExceptions.indexOf(username) !== -1)
		return;

	var clbutt = 'signature-' + username.replace(/ /g, '_');
	var minisig = el('span', [
		el('span', ['—'], { 'class': 'dash' }),
		'\xa0',
		mw.util.isIPv4Address(username) || mw.util.isIPv6Address(username)
			? el('span', [username], { 'class': 'username' })
			: link([username], mw.util.getUrl('User:' + username), { 'class': 'username' }),
		annotation ? el('span', [
			' (' + annotation + ')'
		], { "class": "annotation" }) : void(null),
		' ',
		el('small', [
			'(',
			cwrap('tlk-link', [link(['talk'], mw.util.getUrl('User talk:' + username),
				'Discussion page of this user'
			)]),
			cwrap('ctb-link', ['\xa0•\xa0', link(['contrib'], mw.util.getUrl('Special:Contributions/' + username),
				'Contributions of this user'
			)]),
			cwrap('act-link', ['\xa0•\xa0', link(['actions'], mw.util.getUrl('Special:Log', { user: username }),
				'Actions performed by this user'
			)]),
			cwrap('log-link', ['\xa0•\xa0', link(['log'], mw.util.getUrl('Special:Log', { page: 'User:' + username }),
				'Actions performed on this user'
			)]),
			cwrap('sig-link', ['\xa0•\xa0', link(['sig'], function (ev) {
				wrapper.style.display = wrapper.style.display === 'none' ? 'inline-block' : 'none';
				if (wrapper.style.display !== 'none') {
					var msd = displacement(minisig);
					var wsd = displacement(wrapper.offsetParent);
					wrapper.style.top = (msd[1] - wsd[1] + minisig.offsetHeight + 4) + 'px';
					wrapper.style.left = (msd[0] - wsd[0]) + 'px';
					wrapper.style.position = 'absolute !important';
				}
			}, 'Show original signature')]),
			')'
		]),
	], { 'class': 'kephir-unclutter-minisig signature ' + clbutt + ' ' + unameClasses(username) });

	wrapper.style.cssText += '; position: absolute !important; z-index: 69;';
	wrapper.style.display = 'none';
	wrapper.parentNode.insertBefore(document.createTextNode(' '), wrapper);
	wrapper.parentNode.insertBefore(minisig, wrapper);
	wrapper.parentNode.insertBefore(document.createTextNode(' '), wrapper);
	minisig.insertBefore(wrapper, minisig.firstChild);
	wrapper.style.background = '#ffffcc';
	wrapper.style.border = '1px solid black';
	wrapper.style.padding = '0.2em';

	return minisig;
}

function processLiquidSignature(lqtwrapper) {
	// determine username
	var siglinks = lqtwrapper.getElementsByTagName('a');
	var m, username, oldname, tparams = {};

	// the checks are much lighter here, since we are much more sure that this is a signature
	for (var k = 0; k < siglinks.length; ++k) {
		var title = targetPage(siglinks[k].href, tparams);
		if (!title)
			continue;

		if ((title.getNamespaceId() === wgConf.wgNamespaceIds.user) || (title.getNamespaceId() === wgConf.wgNamespaceIds.user_talk))
			oldname = username = title.getMainText().replace(/\/.*$/, '');
		else if (title.getNamespaceId() === wgConf.wgNamespaceIds.special)
			if (m = /^(Contributions|EmailUser|Log(?:\/delete|\/protect|\/block|))\/(?![a-z])([^?&#/]+)$/.exec(title.getMainText())) {
				if (m[1] === 'EmailUser')
					m[2] = m[2].replace(/^User:/i, '');
				oldname = username = m[2];
			} else
				continue;
		else
			continue;

		if (siglinks[k].classList.contains('mw-redirect')) {
			try {
				username = solveRedirect(oldname);
			} catch (e) {}
		}
		break;
	}

	if (!username)
		return; // skip. what else can we do?

	var wrapper = document.createElement('span');
	lqtwrapper.insertBefore(wrapper, lqtwrapper.firstChild);
	while (wrapper.nextSibling)
		wrapper.appendChild(wrapper.nextSibling);

	var context = {
		sigOriginal: wrapper,
		oldUsername: oldname,
		username: username,
		annotation: settings.userAnnotations[username],
		colourise: settings.signaturesColourise,
		colour: userColours[username] || (userColoursQueue[username] ? userColoursQueue[username].colour : void(0)),
		lqtWrapper: lqtwrapper,
		lqtTimestamp: lqtwrapper.nextSibling,
	};

	while (lqtwrapper = lqtwrapper.parentNode)
		if (lqtwrapper.classList.contains('lqt-post-wrapper'))
			break;
	context.lqtPostWrapper = lqtwrapper;
	lqtwrapper.classList.add('post-' + escapeClass(username.replace(/ /g, '_')));

	if (settings.processSig(context) !== true) {
		if (!context.annotation && (oldname !== username))
			context.annotation = oldname;
		context.sigMinimised = minimiseSig(wrapper, username, context.annotation);
	}
	settings.postProcessSig(context);
	if (context.colourise && !userColours[username]) {
		var uq = userColoursQueue[username] = userColoursQueue[username] || { count: 0 };
		uq.colour = context.colour;
		uq.count++;
	}
}

function assignColours() {
	if (!userColoursSheet)
		return;

	var queue = Object.keys(userColoursQueue).sort(function (apple, orange) {
		if (userColoursQueue[apple].colour && !userColoursQueue[orange].count)
			return -1;
		else if (!userColoursQueue[apple].colour && userColoursQueue[orange].count)
			return 1;
		return userColoursQueue[apple].count - userColoursQueue[orange].count;
	});

	for (var j = 0; j < queue.length; ++j) {
		var colour = (userColoursQueue[queue[j]].colour || pickSigColour());
		userColours[queue[j]] = String(colour);
		userLQTColours[queue[j]] = String(colour.lqtVariant || colour);
		userColoursSheet.addRule(
			'.kephir-unclutter-minisig.signature-' + escapeClass(queue[j].replace(/ /g, '_')),
			'background: ' + colour
		);
		if (settings.liquidThreadsColourPosts) {
			userColoursSheet.addRule(
				'.lqt-post-wrapper.post-' + escapeClass(queue[j].replace(/ /g, '_')),
				'background: ' + colour.lqtVariant || colour
			);
		}
	}
	
	userColoursQueue = {};

	try {
		if (!Object.keys(userColours).length)
			return;
		window.sessionStorage.setItem('kephir-unclutter-sigcolours-' + wgConf.wgPageName, JSON.stringify(userColours));
		window.sessionStorage.setItem('kephir-unclutter-lqtcolours-' + wgConf.wgPageName, JSON.stringify(userLQTColours));
	} catch (e) {
		/* ignore */
	}
}

if (((wgConf.wgNamespaceNumber > 0) || (wgConf.wgCanonicalSpecialPageName === 'NewMessages')) && (wgConf.wgAction === 'view') && settings.signaturesProcess) {
	if (window.liquidThreads) { /* lovely. */
		var sigs = document.getElementsByClassName('lqt-thread-user-signature');
		for (var j = 0; j < sigs.length; ++j) {
			processLiquidSignature(sigs[j]);
		}

		var _setupThread = window.liquidThreads.setupThread;
		window.liquidThreads.setupThread = function (container) {
			var result = _setupThread.apply(this, arguments);
			var sigs = container[0].getElementsByClassName('lqt-thread-user-signature');
			for (var j = 0; j < sigs.length; ++j) {
				processLiquidSignature(sigs[j]);
			}
			assignColours();
			return result;
		};

		assignColours();

		return;
	}

	var links = (document.getElementById('mw-content-text') || { getElementsByTagName: function () { return []; } }).getElementsByTagName('a');
	for (var i = 0; i < links.length; ++i) {
		var m;
		if (links[i].processedSig)
			continue;

		var tparams = {};
		var title = targetPage(links[i].href, tparams);
		if (!title)
			continue;
		var oldname;

		if (tparams.redlink ? tparams.action !== 'edit' : tparams.action)
			continue;

		if ((title.getNamespaceId() === wgConf.wgNamespaceIds.user) || (title.getNamespaceId() === wgConf.wgNamespaceIds.user_talk))
			if (title.fragment && !/^(top|no ?bold)$/i.test(title.fragment))
				continue;
			else if (title.getPrefixedDb().indexOf('/') !== -1)
				continue;
			else
				oldname = title.getMainText();
		else if (title.getNamespaceId() === wgConf.wgNamespaceIds.special)
			if (m = /^(Contributions|EmailUser|Log(?:\/delete|\/protect|\/block|))\/(?![a-z])([^?&#/]+)$/.exec(title.getMainText())) {
				if (m[1] === 'EmailUser')
					m[2] = m[2].replace(/^User:/i, '');
				oldname = m[2];
			} else
				continue;
		else
			continue;

		var username = oldname;
		if (links[i].classList.contains('mw-redirect')) {
			try {
				username = solveRedirect(oldname);
			} catch (e) {}
		}
		var wrapper = wrapSignature(links[i], username);
		if (wrapper) {
			var siglinks = wrapper.getElementsByTagName('a');
			var minisig = null;
			for (var j = 0; j < siglinks.length; ++j)
				siglinks[j].processedSig = true;

			var context = {
				sigOriginal: wrapper,
				oldUsername: oldname,
				username: username,
				annotation: settings.userAnnotations[username],
				colourise: settings.signaturesColourise,
				colour: userColours[username] || (userColoursQueue[username] ? userColoursQueue[username].colour : void(0))
			};
			if (settings.processSig(context) !== true) {
				if (!context.annotation && (oldname !== username))
					context.annotation = oldname;
				context.sigMinimised = minimiseSig(wrapper, username, context.annotation, context.colourise ? context.colour : null);
			}
			settings.postProcessSig(context);
			if (context.colourise && !userColours[username]) {
				var uq = userColoursQueue[username] = userColoursQueue[username] || { count: 0 };
				uq.colour = context.colour;
				uq.count++;
			}

			if (minisig) {
				var siglinks = minisig.getElementsByTagName('a');
				for (var j = 0; j < siglinks.length; ++j)
					siglinks[j].processedSig = true;
			}
		}
	}
	assignColours();
}

});