MediaWiki:Gadget-aWa.js

Note – after saving, you may have to bypass your browser’s cache to see the changes.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.
Please think of the kittens.

This is the aWa gadget. To enable it, go to Special:Preferences, check the "aWa, a script to assist in archiving discussions in WT:RFD, WT:RFDO, WT:RFV, WT:RFC, WT:RFM" checkbox, and click [Save]. Only autopatrolled users can enable the gadget in their preferences.

  • How to use it: go to WT:RFV, WT:RFD, WT:RFDO, WT:RFC, or WT:RFM. For each level-two section header, an "[archive]" link will appear next to it. Click on it to mark the section for archiving. You can probably figure out the rest.
  • Discussions are usually archived seven days after closure, to give everyone time to raise any objections: more controversial discussions may be kept for longer.
  • The gadget seems stable now, although the code uses some crude hacks and may need optimising (look for "XXX"). The basic structure and core functionality is rather clean, however. Improvements to the UI are also welcome.

See also


"use strict"; /*** [aWa]: A Wonderfool Archiver ***/ 
// {{documentation}} <nowiki>
/*jshint shadow:true, scripturl:true, undef:true, latedef:true, unused:true */
/*global mw, jQuery */
(function () {

// temporarily disable aWa due to a bug where it archives to the talk page of the discussion page on which it is used
if (mw.config.get('wgAction') !== 'view')
	return;

var monthNames = [
	"January", "February", "March", "April", "May", "June", "July",
	"August", "September", "October", "November", "December"
];
var revMonthNames = {};
for (var j = 0; j < monthNames.length; ++j)
	revMonthNames[monthNames[j]] = j;

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;
}

var forumId, remindersList = [];
var wgPageName = mw.config.get('wgPageName');
if (/^Wiktionary:Requests_for_verification/.test(wgPageName)) {
	forumId = 'rfv';
	remindersList.push(["Remember to remove the ", el('code', "{{rfv}}"), " and/or ", el('code', "{{rfv-sense}}"), " template from verified entries."]);
} else if ((wgPageName === "Wiktionary:Requests_for_deletion") || /^Wiktionary:Requests_for_deletion/.test(wgPageName)) {
	forumId = 'rfd';
	remindersList.push(["Remember to remove the ", el('code', "{{rfd}}"), " and/or ", el('code', "{{rfd-sense}}"), " template from kept entries."]);
} else if (wgPageName === "Wiktionary:Requests_for_deletion/Others") {
	forumId = 'rfdo';
	remindersList.push(["Remember to remove the ", el('code', "{{rfd}}"), " template from kept pages."]);
} else if ((wgPageName === "Wiktionary:Requests_for_moves,_mergers_and_splits") || /^Wiktionary:Requests_for_moves,_mergers_and_splits\/Unresolved_requests\//.test(wgPageName)) {
	forumId = 'rfm';
	remindersList.push(["Remember to remove the ", el('code', "{{rfm}}"), " template from moved pages."]);
} else if ((wgPageName === "Wiktionary:Requests_for_cleanup") || /^Wiktionary:Requests_for_cleanup\/archive\/\d+\/Unresolved_requests$/.test(wgPageName)) {
	forumId = 'rfc';
	remindersList.push(["Remember to remove the ", el('code', "{{rfc}}"), " template from pages which have been dealt with."]);
} else if (wgPageName === "Wiktionary:Feedback") {
	forumId = 'feedback';
	remindersList.push(["Feedback is rarely archived at all; old feedback is usually just removed."]);
} else if (wgPageName === "Wiktionary:Sandbox/aWa") {
	forumId = 'SANDBOX';
	remindersList.push(["NO DANGER: Nothing in this room will kill you."]);
} else
	return;

if (forumId !== 'feedback')
	remindersList.push(["Keep closed discussions unarchived for at least 7 days."]);

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);
}

var rxPath = new RegExp('^' + mw.config.get('wgArticlePath').replace('$1', '(.*)') + '$'); // XXX: ugly hack
function getPageFromUrl(url) {
	var m, target;
	try {
		target = new mw.Uri(url);
	} catch (e) {
		return null;
	}
	if (target.host !== location.host)
		return null;
	if (target.path === mw.config.get('wgScript'))
		return new mw.Title(target.query.title);
	else if ((m = rxPath.exec(target.path)))
		return new mw.Title(decodeURIComponent(m[1]));
	return null;
}

function findSectionNumber(header) {
	var links = header.getElementsByTagName('a');
	for (var j = 0; j < links.length; ++j) {
		if (links[j].parentNode.className !== 'mw-editsection')
			continue;
		var uri = new mw.Uri(links[j].href);
		if ((uri.path === mw.config.get('wgScript')) && (uri.query.action === 'edit') && (uri.query.title === wgPageName))
			return Number(uri.query.section);
	}
	return null;
}

var api = new mw.Api();

// XXX: seriously, action=parse should return this.
var starttimestamp, basetimestamp;
api.get({
	action: 'query',
	pageids: mw.config.get('wgArticleId'),
	prop: 'revisions',
	curtimestamp: 1,
	rvstartid: mw.config.get('wgRevisionId'),
	rvprop: 'timestamp',
	rvlimit: 1
}).done(function (result) {
	starttimestamp = result.curtimestamp;
	basetimestamp = result.query.pages[mw.config.get('wgArticleId')].revisions[0].timestamp;
});

var toArchive = {};
var uiQueue, uiStatus;
function setStatus(child, node) {
	node = node || uiStatus;
	if (!node.preceder)
		node.parentNode.insertBefore(node.preceder = document.createTextNode(''), node);
	while (node.firstChild)
		node.removeChild(node.firstChild);
	if (!child) {
		if (node.preceder !== true)
			node.preceder.data = "";
		return;
	} else {
		if (node.preceder !== true)
			node.preceder.data = ": ";
	}

	if (typeof child === 'string') {
		node.appendChild(document.createTextNode(child));
		return;
	}

	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);
	}
}

var uiProceed, uiWrapper;
var uiArchiver = el('div', [
	el('form', [
		uiWrapper = el('fieldset', [
			//  ↓↓↓↓↓↓ how fitting.
			el('legend', [
				"A ", link(["Wonderfool"], mw.util.getUrl("wonderfool")), " Archiver ",
				el('small', ["[", link('collapse', function () {
					this.style.fontWeight = uiWrapper.classList.toggle('collapsed') ? 'bold' : '';
				}), "]"])
			]),
			uiQueue = el('ul', null, { 'class': 'queue' }),
			el('ul', remindersList.map(function (nodes) {
				return el('li', nodes);
			}), { 'class': 'reminders' }),
			uiProceed = el('input', null, { type: 'submit', value: "Proceed" }), ' ',
			el('input', null, { type: 'button', value: "Close" }, {
				click: function () {
					uiArchiver.style.display = 'none';
				}
			}),
			uiStatus = el('p', null, { 'class': 'main-statusbar' })
		])
	], { action: 'javascript:void(0);' }, {
		submit: function () {
			if (!uiQueue.hasChildNodes()) { // XXX: should probably check the toArchive object instead
				setStatus("The archiving queue is empty.");
				return;
			}

			if (forumId !== 'SANDBOX') {
				uiProceed.disabled = true;
				uiArchiver.classList.add('archiving-in-progress');
			}
			var complete = false;
			window.addEventListener('beforeunload', function (ev) {
				if (!complete) {
					return (ev.returnValue = "Discussions are still being archived.");
				}
			}, false);
			
			setStatus("Grabbing page text…");
			api.get({
				action: 'parse',
				oldid: mw.config.get('wgRevisionId'),
				prop: 'parsetree',
			}).then(function (result) {
				setStatus("Slicing up discussions…");
				var prettyPageName = (new mw.Title(wgPageName)).getPrefixedText();
				
				function serialiseMarkup(node) {
					if (node.nodeType === node.TEXT_NODE)
						return node.data;

					var children = [];
					
					for (var child = node.firstChild; child; child = child.nextSibling)
						children.push(serialiseMarkup(child));
					
					if (node.tagName === 'part')
						return '|' + children.join('');
					else if (node.tagName === 'tplarg')
						return '{{{' + children.join('') + '}}}';
					else if (node.tagName === 'template')
						return '{{' + children.join('') + '}}';
					else if (node.tagName === 'ext')
						return '<' + children.join('');
					else if (node.tagName === 'attr')
						return children.join('') + (node.parentNode.lastChild === node ? '/>' : '');
					else if (node.tagName === 'inner')
						return '>' + children.join('');
					else
						return children.join('');
				}
				
				var parseTree = jQuery.parseXML(result.parse.parsetree['*']);

				var headerMap = {};
				var headers = parseTree.getElementsByTagName('h');
				for (var i = 0; i < headers.length; ++i) {
					headerMap[headers[i].getAttribute('i')] = headers[i];
				}

				var majorQueue = [], minorQueue = [], obliterated = [];
				var keys = Object.keys(toArchive);
				for (var j = 0; j < keys.length; ++j) {
					var item = toArchive[keys[j]];
					var sectHead = headerMap[item.sect];
					var sectContents = [];
					
					for (
						var node = sectHead, next = sectHead.nextSibling;
						node && (!node.getAttribute || !node.getAttribute('level') || (+node.getAttribute('level') > +sectHead.getAttribute('level')) || (node === sectHead));
						(next = node.nextSibling, node.parentNode.removeChild(node), node = next)
					) {
						sectContents.push(serialiseMarkup(node));
					}
					
					var secttext = sectContents.join('');

					var discussionTitle;

					switch (forumId) {
					case 'rfd'     : discussionTitle = "[[" + prettyPageName + "|RFD]] discussion"             ; break;
					case 'rfdo'    : discussionTitle = "[[" + prettyPageName + "|RFDO]] discussion"            ; break;
					case 'rfv'     : discussionTitle = "[[" + prettyPageName + "|RFV]] discussion"             ; break;
					case 'rfc'     : discussionTitle = "[[" + prettyPageName + "|RFC]] discussion"             ; break;
					case 'rfm'     : discussionTitle = "[[" + prettyPageName + "|RFM]] discussion"             ; break;
					case 'rft'     : discussionTitle = "Archived from the [[" + prettyPageName + "|Tea Room]]" ; break;
					case 'feedback': discussionTitle = "Archived [[" + prettyPageName + "|feedback]]"          ; break;
					default        : discussionTitle = "Archived from [[" + prettyPageName + "]]"              ; break;
					}

					var rxStamp = /(\d\d?):(\d\d),\s*(\d+)\s*([A-Z][a-z]+)\s*(\d\d\d\d+)\s*\(UTC\)/g;
					var m, timestamps = [];
					while ((m = rxStamp.exec(secttext)))
						timestamps.push(Date.UTC(parseInt(m[5], 10), revMonthNames[m[4]], parseInt(m[3], 10), parseInt(m[1], 10), parseInt(m[2], 10)));
					timestamps.sort();

					if (timestamps.length > 0) {
						var first = new Date(timestamps[0]), last = new Date(timestamps[timestamps.length - 1]);
						if (first.getUTCFullYear() === last.getUTCFullYear()) {
							if (first.getUTCMonth() === last.getUTCMonth())
								discussionTitle += ": " + monthNames[first.getUTCMonth()] + " " + first.getUTCFullYear();
							else
								discussionTitle += ": " + monthNames[first.getUTCMonth()] + "–" + monthNames[last.getUTCMonth()] + " " + first.getUTCFullYear();
						} else {
							discussionTitle += ": " + monthNames[first.getUTCMonth()] + " " + first.getUTCFullYear() + '–' +
								monthNames[last.getUTCMonth()] + " " + last.getUTCFullYear();
						}
					}

					if (!item.talk) {
						obliterated.push(item.secttitle);
						continue; // requested not to archive.
					}
					
					secttext = secttext.replace(/^(=+)(.*?)\1(?=\n|$)/, '{{fake$1|1=$2}}');

					majorQueue.push({
						targetPage: item.talk,
						uiStatus: item.uiStatus,
						sectTitle: discussionTitle,
						summary: '[[MediaWiki:Gadget-aWa.js/documentation|aWa]]: Archiving discussions from [[' + prettyPageName + ']]',
						content: '{{archive-top|' + forumId + '|' + item.result +
							'|link=Special:PermanentLink/' + mw.config.get('wgRevisionId') + '#' + item.sectid + '}}\n' +
							secttext.trimRight() +
							'\n{{archive-bottom}}'
					});

					var backlinks = item.backlinks;
					for (var k = 0; k < backlinks.length; ++k) {
						if (!backlinks[k])
							continue;

						var saneDiscussionTitle = discussionTitle.replace(/\[\[(?:.*?\|)?(.*?)\]\]/g, '$1'); // XXX
						var linkTarget = item.talk.getPrefixedText() + "#" + saneDiscussionTitle;

						minorQueue.push({
							targetPage: backlinks[k].page,
							uiStatus: backlinks[k].uiStatus,
							sectTitle: discussionTitle,
							summary: "[[MediaWiki:Gadget-aWa.js/documentation|aWa]]: Adding link to a discussion at [[" + linkTarget + "]], archived from [[" + prettyPageName + "]]",
							content: ": ''See [[" + linkTarget + "]].''"
						});
					}
				}
				
				var headers = Array.prototype.slice.call(parseTree.getElementsByTagName('h'));
				for (var i = 0; i < headers.length; ++i) {
					if (headers[i].getAttribute('level') !== '1')
						continue;
					if (!/^=\s*(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d\d\d\d+\s*=$/.test(headers[i].textContent))
						continue;
					
					out: for (var node = headers[i].nextSibling;; node = node.nextSibling) {
						switch (node ? node.nodeType : null) {
						case parseTree.ELEMENT_NODE:
							if ((node.tagName !== 'h') || (node.getAttribute('level') !== '1'))
								break out;
							/* fall through */
						case null:
							while ((headers[i].nextSibling) !== node)
								headers[i].parentNode.removeChild(headers[i].nextSibling);
							headers[i].parentNode.removeChild(headers[i]);
							break out;
						case parseTree.TEXT_NODE:
							if (!/^\s*$/.test(node.data))
								break out;
							break;
						default:
							break out;
						}
					}
				}

				var wikitext = serialiseMarkup(parseTree.documentElement);

				setStatus("Removing discussions from this page…");

				var summary;
				if (majorQueue.length)
					summary = '[[MediaWiki:Gadget-aWa.js/documentation|aWa]]: Archiving ' + majorQueue.length + ' discussion(s) to ' +
						majorQueue.map(function (item) { return '[[' + item.targetPage.getPrefixedText() + ']]'; }).join(", ") +
						(obliterated.length ? " and removing " + obliterated.length + " other(s): \"" + obliterated.join("\", \"") + "\"": "");
				else
					summary = "[[MediaWiki:Gadget-aWa.js/documentation|aWa]]: Removing " + obliterated.length + " discussion(s): \"" + obliterated.join("\", \"") + "\"";
					
				if (forumId === 'SANDBOX') {
					setStatus();
					alert("Summary: " + summary + "\nFinal wikitext:\n\n" + wikitext);
					majorQueue.forEach(function (item) {
						alert("[[" + item.targetPage.getPrefixedText() + "]]: " + item.sectTitle + "\n\n" + item.content);
					});
					minorQueue.forEach(function (item) {
						alert("[[" + item.targetPage.getPrefixedText() + "]]: " + item.sectTitle + "\n\n" + item.content);
					});
					return;
				}

				api.post({
					action: 'edit',
					pageid: mw.config.get('wgArticleId'),
					token: mw.user.tokens.get('csrfToken'),
					summary: summary,
					notminor: 1,
					text: wikitext,
					basetimestamp: basetimestamp,
					starttimestamp: starttimestamp
				}).then(function (result) {
					function processQueue(queue) {
						return jQuery.when.apply(jQuery, queue.map(function (item) {
							setStatus("Saving…", item.uiStatus);
							return jQuery.Deferred(function (d) {
								api.post({
									action: 'edit',
									token: mw.user.tokens.get('csrfToken'),
									notminor: 1,
									section: 'new',
									sectiontitle: item.sectTitle,
									title: item.targetPage.getPrefixedDb(),
									text: item.content,
									summary: item.summary,
									redirect: 1 // XXX
								}).then(function (result) {
									if (result.edit.result !== 'Success') {
										setStatus("Failed, see console", item.uiStatus);
										d.reject();
										console.error(result);
										return;
									}
	
									// XXX: preferably check if a page is a redirect BEFORE archiving.
									if (result.redirects) {
										var target = result.redirects.pop().to;
										setStatus(["Redirected to ", link(target, mw.util.getUrl(target))], item.uiStatus);
										d.resolve();
										return;
									}
	
									setStatus("OK", item.uiStatus);
									d.resolve();
								}, function (code, result) {
									setStatus(["Error: [", el('code', [code]), "]"], item.uiStatus);
									console.error('Request error', result);
									d.reject();
								});
							}).promise();
						}));
					}
					
					if (result.edit.result !== 'Success') {
						setStatus("Unexpected failure. See console for details.");
						console.error(result);
						complete = true;
						return;
					}
					
					setStatus("Putting archived discussions on talk pages…");
					processQueue(majorQueue).then(function () {
						setStatus("Adding backlinks…");
						processQueue(minorQueue).then(function () {
							setStatus("Done. Reloading…");
							complete = true;
							location.reload();
						}, function () {
							complete = true;
							setStatus("Archiving was successful, but there was an error while adding backlinks. You can visit the talk pages to add backlinks manually.");
						});
					}, function () {
						complete = true;
						setStatus([
							"Oops… there was an error while archiving discussions. Go to your ",
							link("contributions list", mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))),
							" to clean things up and try again."
						]);
					});
				}, function (code, result) {
					complete = true;
					if (code === 'editconflict') {
						setStatus([
							'Edit conflict detected. Please ',
							link('refresh the page', function () {
								location.reload();
							}),
							' and try again.'
						]);
						return;
					}
					if (code === 'http') {
						setStatus(['Request error: ' + result.textStatus]);
						console.error('API request error: ', result);
						return;
					}
					if (code === 'ok-but-empty') {
						setStatus(['Returned empty response from server.']);
						return;
					}
					setStatus([
						'Error while saving the main discussion page: ',
						result.error.info,
						' [code ', el('code', result.error.code), ']'
					]);
					console.error(arguments);
				});
			}, function (code, result) {
				if (code === 'http') {
					setStatus('Request error: ' + result.textStatus);
					console.error('API request error: ', result);
					return;
				}
				if (code === 'ok-but-empty') {
					setStatus('Received empty response from server.');
					return;
				}
				setStatus(['Error: ', result.error.info, ' [code ', el('code', [result.error.code]), ']']);
				return;
			});
		}
	})
], {
	'class': 'kephir-awa-dialog',
	'style': 'display: none;'
});
document.body.appendChild(uiArchiver);
uiStatus.preceder = true;

var headers = document.getElementsByClassName('mw-headline');

var dupTracker = {};

function makeLinkFor(headerNode, links, age) {
	var section = findSectionNumber(headerNode.parentNode);
	var targetChecked = false, targetCheckRequest;
	
	if (section === null)
		return;

	headerNode.parentNode.appendChild(el('span', [
		'[', link('archive', function () {
			var that = this, config = {
				secttitle: headerNode.textContent,
				sect: section,
				sectid: headerNode.id,
				temp: 'archived',
				backlinks: []
			};
			setStatus();
			uiArchiver.style.display = '';
			if (this.item)
				return;
			toArchive[section] = config;

			var uiArchiveTarget, uiArchiveTargetLink, uiArchiveNowhereLink;
			var uiResult, results = {};
			var uiBacklinks, uiAddBacklink, uiLinksFromSel;

			function setTarget(page, noCheck) {
				if (!page) {
					uiArchiveTarget.style.display = 'none';
					uiArchiveNowhereLink.classList.add('active-mode');
					uiBacklinks.classList.add('irrelevant');
					uiAddBacklink.classList.add('irrelevant');
					uiLinksFromSel.classList.add('irrelevant');
					if (uiResult) uiResult.classList.add('irrelevant');
					if (!config.talk)
						return;
					delete dupTracker[config.talk.getPrefixedDb()];
					config.talk = null;
					return;
				}

				config.talk = page;
				dupTracker[page.getPrefixedDb()] = true;
				uiArchiveTarget.style.display = '';
				uiArchiveTargetLink.textContent = page.getPrefixedText();
				uiArchiveTargetLink.href = mw.util.getUrl(page.getPrefixedDb());
				uiArchiveNowhereLink.classList.remove('active-mode');
				uiBacklinks.classList.remove('irrelevant');
				uiAddBacklink.classList.remove('irrelevant');
				uiLinksFromSel.classList.remove('irrelevant');
				if (uiResult) uiResult.classList.remove('irrelevant');

				if (!noCheck) {
					targetCheckRequest = api.get({
						action: 'query',
						titles: page.getPrefixedDb(),
						redirects: 1
					}).then(function (result) {
						if (result.query.redirects) {
							setStatus([
								"Archive destination [[",
								link(page.getPrefixedText(), mw.util.getUrl(page.getPrefixedDb(), { 'redirect': 'no' })),
								"]] is a redirect; corrected."
							]);
							setTarget(new mw.Title(result.query.redirects.pop().to), true);
						}
						targetChecked = true;
						targetCheckRequest = null;
					});
				} else
					targetChecked = true;
			}
			function addBacklink(page, noCheck) {
				if (!page)
					return;

				var uiItem, blConfig = {};
				var i = config.backlinks.length;
				config.backlinks.push(blConfig);

				var uiLink;

				function changePage(newPage, noCheck) {
					blConfig.page = newPage;
					blConfig.checked = noCheck;
					uiLink.textContent = newPage.getPrefixedText();
					dupTracker[newPage.getPrefixedDb()] = true;

					if (!noCheck) {
						blConfig.checkRequest = api.get({
							action: 'query',
							titles: newPage.getPrefixedDb(),
							redirects: 1
						}).then(function (result) {
							if (result.query.redirects) {
								setStatus([
									"Backlink destination [[",
									link(newPage.getPrefixedText(), mw.util.getUrl(newPage.getPrefixedDb(), { 'redirect': 'no' })),
									"]] is a redirect; corrected."
								]);
								changePage(new mw.Title(result.query.redirects.pop().to), true);
								page = blConfig.page;
							}
							blConfig.checked = true;
							blConfig.checkRequest = null;
						});
					}
				}

				uiBacklinks.appendChild(uiItem = el('li', [
					uiLink = link(page.getPrefixedText(), mw.util.getUrl(page.getPrefixedDb()), {
						'title': "A link back to the discussion will be put at this page"
					}),
					el('span', [
						" (", link('cancel', function () {
							uiItem.parentNode.removeChild(uiItem);
							delete config.backlinks[i];
							delete dupTracker[page.getPrefixedDb()];
						}, { 'title': "Do not put a backlink on that page" }),
						" \u2022 ", link("swap", function () {
							if (!config.talk) {
								uiItem.parentNode.removeChild(uiItem);
								delete config.backlinks[i];
								if (blConfig.checkRequest) blConfig.checkRequest.abort();
								setTarget(page, blConfig.checked);
								return;
							}
							if (blConfig.checkRequest) blConfig.checkRequest.abort();
							if (targetCheckRequest) targetCheckRequest.abort();
							changePage(config.talk, targetChecked);
							setTarget(page, blConfig.checked);
							page = blConfig.page;
						}, { 'title': "Swap this page with the archive destination" }), ")"
					], { 'class': 'hide-when-archiving' }),
					blConfig.uiStatus = el('span')
				]));

				changePage(page, noCheck);

				return i;
			}
			function setResult(result) {
				for (var key in results)
					results[key].classList.remove('active-mode');
				if (results[result])
					results[result].classList.add('active-mode');
				config.result = result;
			}

			uiQueue.appendChild(this.item = el('li', [
				'"', link(headerNode.textContent, '#' + headerNode.id, {
					'title': "Click to view this discussion"
				}), '"',
				uiArchiveTarget = el('span', [
					"→ [[", uiArchiveTargetLink = link('?',  'javascript:void(window.warranty);', {
						'title': "The discussion will be archived at this page"
					}), "]]"
				]),
				config.uiStatus = el('span'),
				el('br'), el('small', [
					link("retarget", function () {
						var newTarget = prompt('New archive target?', config.talk ? config.talk.getPrefixedText() : "");
						if (!newTarget)
							return;
						try {
							newTarget = new mw.Title(newTarget);
						} catch (e) {
							setStatus("Not a valid page title");
							return;
						}
						if (newTarget.getNamespaceId() < 0)	{
							setStatus("Cannot archive into a special namespace");
							return;
						}
						newTarget.namespace |= 1;

						setTarget(newTarget);
						setStatus();
					}, { 'title': "Change the page where the discussion is to be archived" }),
					" \u2022 ", uiArchiveNowhereLink = link("memory hole", function () {
						setTarget(null);
					}, { 'title': "Do not archive this discussion anywhere, just remove" }),
					" \u2022 ", uiAddBacklink = link("link back", function () {
						var newTarget = prompt("Add it where?", '');
						if (!newTarget)
							return;

						try {
							newTarget = new mw.Title(newTarget);
						} catch (e) {
							setStatus("Not a valid page title");
							return;
						}
						if (newTarget.getNamespaceId() < 0)	{
							setStatus("Cannot archive into a special namespace");
							return;
						}
						newTarget.namespace |= 1;

						addBacklink(newTarget);
						setStatus();
					}, { 'title': "Add a page where a link back to this discussion is to be put" }),
					" \u2022 ", uiLinksFromSel = link("from selection", function () {
						var sel = window.getSelection(), taken = {};
						
						var intersectsNode = function (node) {
							var tmp = document.createRange();
							tmp.selectNode(node);
							return (this.compareBoundaryPoints(this.START_TO_START, tmp) >= 0)
								&& (this.compareBoundaryPoints(this.END_TO_END, tmp) <= 0);
						};
						
						for (var i = 0; i < sel.rangeCount; ++i) {
							var range = sel.getRangeAt(i);
							var links = range.commonAncestorContainer.getElementsByTagName('a');
							if (!range.intersectsNode)
								range.intersectsNode = intersectsNode;
							for (var j = 0; j < links.length; ++j) {
								if (!range.intersectsNode(links[j]))
									continue;
								var title = getPageFromUrl(links[j].href);

								if (!title || (title.getNamespaceId() < 0))
									continue;
								
								if (title) {
									title.namespace |= 1;
									if (taken[title.getPrefixedDb()])
										continue;
									addBacklink(title);
									taken[title.getPrefixedDb()] = true;
								}
							}
							range.detach();
						}
					}, { 'title': "Add backlinks at pages linked in current selection" }),
					" \u2022 ", link("cancel", function () {
						that.item.parentNode.removeChild(that.item);
						delete toArchive[section];
						delete that.item;
					}, { 'title': "Do not archive this discussion" }),
					((forumId === 'rfd') || (forumId === 'rfdo') || (forumId === 'rfv') || (forumId === 'SANDBOX')) ? uiResult = el('span', [
						" \u2022 result: ", results.passed = link("pass", function () {
							setResult('passed');
						}, { 'title': "Mark this discussion as resolved successfully (verified, consensus to keep)" }),
						" \u2022 ", results.failed = link("fail", function () {
							setResult('failed');
						}, { 'title': "Mark this discussion as having failed the process (deleted)" }),
						" \u2022 ", results.archived = link("other", function () {
							setResult('archived');
						}, { 'title': "Do not mark discussion outcome (mixed, no consensus, etc.)" } )
					]) : null
				], { 'class': 'hide-when-archiving' }),
				uiBacklinks = el('ul', [], { 'class': 'kephir-awa-backlinks' })
			]));

			var result = 'archived';
			// Expected HTML elements:
			// <div class="mw-heading ..."><h2><span ... class="mw-headline" ...>
			var mwHeading = headerNode.parentNode.parentNode;
			if (!mwHeading.classList.contains("mw-heading")) {
				console.error("Expected to find element with class mw-heading as parent of %o", headerNode);
			}
			if (results.passed) {
				var resultText;

				for (var node = mwHeading.nextSibling; node; node = node.nextSibling) {
					if (!node.getElementsByClassName)
						continue;
					if (node.classList.contains("mw-heading")
							|| node.getElementsByClassName("mw-heading").length > 0)
						break;
					
					var bs = node.getElementsByTagName('b');
					for (var j = 0; j < bs.length; ++j) {
						var b = bs[j];
						resultText = b.textContent || b.innerText;
						if (/no\s+consensus/i.test(b.parentNode.textContent))
							result = 'archived';
						else {
							if (/\b(failed|deleted|speedied|not\s+restored)\b/i.test(resultText)) {
								result = 'failed';
							} else if (/\b(passed|kept|cited|restored|undeleted)\b/i.test(resultText)) {
								result = 'passed';
							}
						}
					}
				}
			}
			setResult(result);
			
			if (links[0]) {
				var title = getPageFromUrl(links[0].href);
				if (title.getNamespaceId() >= 0) {
					title.namespace |= 1;
					setTarget(title);
				} else
					setTarget(null);
			} else {
				setTarget(null);
			}

			for (var i = 1; i < links.length; ++i) {
				var title = getPageFromUrl(links[i].href);
				title.namespace |= 1;
				addBacklink(title);
			}
		}),
		age ? el('small', [' (age: ' + age + ')']) : void(0),
		']'
	], { "class": "mw-editsection" }));
}

var levels = {};
for (var i = 0; i < headers.length; ++i) {
	var level = parseInt(headers[i].parentNode.tagName.substr(1), 10);
	for (var k = 1; k <= 6; ++k) {
		if (k < level) {
			if (levels[k])
				levels[k].subHeaders.push(headers[i]);
		} else
			delete levels[k];
	}
	levels[level] = headers[i];
	headers[i].subHeaders = [];
}

function collectUntilH2(node) {
	var nodes = [];
	while (node && (node.tagName !== 'H2')) {
		if (node.getElementsByTagName && node.getElementsByTagName('h2').length) {
			return nodes.concat(collectUntilH2(node.firstChild));
		}
		nodes.push(node);
		node = node.nextSibling;
	}
	return nodes;
}

for (var i = 0; i < headers.length; ++i) {
	/*jshint loopfunc:true */
	if (headers[i].parentNode.tagName !== 'H2')
		continue;

	var links = Array.prototype.filter.call(
		headers[i].querySelectorAll('a:not(.ext-discussiontools-init-section-subscribe a)'), function (item) {
			var title = getPageFromUrl(item.href);
			return title && (title.getNamespaceId() >= 0);
		}
	);

	if (!links.length)
		links.unshift(null);

	for (var j = 0; j < headers[i].subHeaders.length; ++j) {
		links = links.concat(Array.prototype.filter.call(headers[i].subHeaders[j].querySelectorAll('a:not(.ext-discussiontools-init-section-subscribe a)'), function (item) {
			var title = getPageFromUrl(item.href);
			return title && (title.getNamespaceId() >= 0);
		}));
	}

	var nodes = collectUntilH2(headers[i].parentNode.parentNode.nextSibling);
	
	var timestamps = Array.prototype.concat.call(nodes.map(function (node) {
		return node.textContent || '';
	}).join('').match(/(\d\d?):(\d\d),\s*(\d+)\s*([A-Z][a-z]+)\s*(\d\d\d\d+)\s*\(UTC\)/g) || [], nodes.map(function(node) {
		// Comments in Local Time
		if (!node.getElementsByClassName)
			return;
		var nodes = Array.prototype.slice.call(node.getElementsByClassName('localcomments'));
		if (/(^|\s)localcomments(\s|$)/.test(node.className))
			nodes.push(node);
		return nodes.map(function (node) {
			return node.title;
		});
	}).reduce(function (accum, item) {
		return item ? accum.concat(item) : accum;
	}, [])).map(function (ts) {
		var m = /(\d\d?):(\d\d),\s*(\d+)\s*([A-Z][a-z]+)\s*(\d\d\d\d+)\s*\(UTC\)/.exec(ts);
		return Date.UTC(parseInt(m[5], 10), revMonthNames[m[4]], parseInt(m[3], 10), parseInt(m[1], 10), parseInt(m[2], 10));
	}).sort();

	var age = null;
	if (timestamps.length) {
		age = Math.floor((Date.now() - timestamps[timestamps.length - 1]) / 86400000) + ' days';
	}

	makeLinkFor(headers[i], links, age);
}

})();
// </nowiki>