MediaWiki:Gadget-popups.js

// .		 * @type Function */		this.startCondition = null;

/**		 * Hook to be run when the drag finishes. This is passed the final coordinates of the * dragged object (two integers, x and y). To disables this, set it to. * @type Function */		this.endHook = null; }

/**	 * Gets an event in a cross-browser manner. * @param {Event} e	 * @private */	Drag.prototype.fixE = function (e) { if (typeof e == 'undefined') { e = window.event; }		if (typeof e.layerX == 'undefined') { e.layerX = e.offsetX; }		if (typeof e.layerY == 'undefined') { e.layerY = e.offsetY; }		return e;	};

/**	 * Initialises the Drag instance by telling it which object you want to be draggable, and what * you want to drag it by. * @param {DOMElement} o The "handle" by which  is dragged. * @param {DOMElement} oRoot The object which moves when  is dragged, or   if omitted. */	Drag.prototype.init = function (o, oRoot) { var dragObj = this; this.obj = o;		o.onmousedown = function (e) { dragObj.start.apply(dragObj, [e]); };		o.dragging = false; o.popups_draggable = true; o.hmode = true; o.vmode = true;

o.root = oRoot ? oRoot : o;

if (isNaN(parseInt(o.root.style.left, 10))) { o.root.style.left = '0px'; }		if (isNaN(parseInt(o.root.style.top, 10))) { o.root.style.top = '0px'; }

o.root.onthisStart = function {}; o.root.onthisEnd = function {}; o.root.onthis = function {}; };

/**	 * Starts the drag. * @private * @param {Event} e	 */ Drag.prototype.start = function (e) { var o = this.obj; // = this; e = this.fixE(e); if (this.startCondition && !this.startCondition(e)) { return; }		var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10); o.root.onthisStart(x, y);

o.lastMouseX = e.clientX; o.lastMouseY = e.clientY;

var dragObj = this; o.onmousemoveDefault = document.onmousemove; o.dragging = true; document.onmousemove = function (e) { dragObj.drag.apply(dragObj, [e]); };		document.onmouseup = function (e) { dragObj.end.apply(dragObj, [e]); };		return false; };

/**	 * Does the drag. * @param {Event} e	 * @private */	Drag.prototype.drag = function (e) { e = this.fixE(e); var o = this.obj;

var ey = e.clientY; var ex = e.clientX; var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10); var nx, ny;

nx = x + (ex - o.lastMouseX) * (o.hmode ? 1 : -1); ny = y + (ey - o.lastMouseY) * (o.vmode ? 1 : -1);

this.obj.root.style[o.hmode ? 'left' : 'right'] = nx + 'px'; this.obj.root.style[o.vmode ? 'top' : 'bottom'] = ny + 'px'; this.obj.lastMouseX = ex; this.obj.lastMouseY = ey;

this.obj.root.onthis(nx, ny); return false; };

/**	 * Ends the drag. * @private */	Drag.prototype.end = function { document.onmousemove = this.obj.onmousemoveDefault; document.onmouseup = null; this.obj.dragging = false; if (this.endHook) { this.endHook(				parseInt(this.obj.root.style[this.obj.hmode ? 'left' : 'right'], 10),				parseInt(this.obj.root.style[this.obj.vmode ? 'top' : 'bottom'], 10)			); }	};	// ENDFILE: domdrag.js

// STARTFILE: structures.js	// pg.structures.original = {}; pg.structures.original.popupLayout = function { return [ 'popupError', 'popupImage', 'popupTopLinks', 'popupTitle', 'popupUserData', 'popupData', 'popupOtherLinks', 'popupRedir', [				'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ],			'popupMiscTools', ['popupRedlink'], 'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab', ];	};	pg.structures.original.popupRedirSpans = function { return [ 'popupRedir', 'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ];	};	pg.structures.original.popupTitle = function (x) { log('defaultstructure.popupTitle'); if (!getValueOf('popupNavLinks')) { return navlinkStringToHTML('< >', x.article, x.params); }		return ''; };	pg.structures.original.popupTopLinks = function (x) { log('defaultstructure.popupTopLinks'); if (getValueOf('popupNavLinks')) { return navLinksHTML(x.article, x.hint, x.params); }		return ''; };	pg.structures.original.popupImage = function (x) { log('original.popupImage, x.article=' + x.article + ', x.navpop.idNumber=' + x.navpop.idNumber); return imageHTML(x.article, x.navpop.idNumber); };	pg.structures.original.popupRedirTitle = pg.structures.original.popupTitle; pg.structures.original.popupRedirTopLinks = pg.structures.original.popupTopLinks;

function copyStructure(oldStructure, newStructure) { pg.structures[newStructure] = {}; for (var prop in pg.structures[oldStructure]) { pg.structures[newStructure][prop] = pg.structures[oldStructure][prop]; }	}

copyStructure('original', 'nostalgia'); pg.structures.nostalgia.popupTopLinks = function (x) { var str = ''; str += '<>';

// user links // contribs - log - count - email - block // count only if applicable; block only if popupAdminLinks str += 'if(user){ <>'; str += 'if(wikimedia){*<>}'; str += 'if(ipuser){}else{*<>}if(admin){*<>}}';

// editing links // talkpage  -> edit|new - history - un|watch - article|edit // other page -> edit - history - un|watch - talk|edit|new var editstr = '<>'; var editOldidStr = 'if(oldid){<>|<>|<>}else{' + editstr + '}';		var historystr = '<>'; var watchstr = '<>|<>';

str += ' if(talk){' + editOldidStr + '|<>' + '*' +			historystr + '*' +			watchstr + '*' +			'<>|<>' + '}else{' + // not a talk page editOldidStr + '*' +			historystr + '*' +			watchstr + '*' +			'<<talk|shortcut=t>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';

// misc links str += ' <<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>'; str += 'if(admin){ }else{*}<<move|shortcut=m>>';

// admin links str += 'if(admin){*<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*' + '<<undelete|undeleteShort>>|<<delete|shortcut=d>>}'; return navlinkStringToHTML(str, x.article, x.params); };	pg.structures.nostalgia.popupRedirTopLinks = pg.structures.nostalgia.popupTopLinks;

/** -- fancy -- **/ copyStructure('original', 'fancy'); pg.structures.fancy.popupTitle = function (x) { return navlinkStringToHTML('<font size=+0>< > ', x.article, x.params); };	pg.structures.fancy.popupTopLinks = function (x) { var hist = '<<history|shortcut=h|hist>>|<<lastEdit|shortcut=/|last>>|<<editors|shortcut=E|eds>>'; var watch = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>'; var move = '<<move|shortcut=m|move>>'; return navlinkStringToHTML(			'if(talk){' +				'<<edit|shortcut=e>>|<<new|shortcut=+|+>>*' +				hist +				'*' +				'<<article|shortcut=a>>|<<editArticle|edit>>' +				'*' +				watch +				'*' +				move +				'}else{<<edit|shortcut=e>>*' +				hist +				'*<<talk|shortcut=t|>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>' +				'*' +				watch +				'*' +				move +				'} ',			x.article,			x.params		); };	pg.structures.fancy.popupOtherLinks = function (x) { var admin = '<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*<<undelete|undeleteShort>>|<<delete|shortcut=d|del>>'; var user = '<<contribs|shortcut=c>>if(wikimedia){|<<count|shortcut=#|#>>}'; user += 'if(ipuser){|< >}else{*<<email|shortcut=E|' + popupString('email') + '>>}if(admin){*<<block|shortcut=b>>}';

var normal = '<<whatLinksHere|shortcut=l|links here>>*<<relatedChanges|shortcut=r|related>>'; return navlinkStringToHTML(			' if(user){' + user + '*}if(admin){' + admin + 'if(user){ }else{*}}' + normal,			x.article,			x.params		); };	pg.structures.fancy.popupRedirTitle = pg.structures.fancy.popupTitle; pg.structures.fancy.popupRedirTopLinks = pg.structures.fancy.popupTopLinks; pg.structures.fancy.popupRedirOtherLinks = pg.structures.fancy.popupOtherLinks;

/** -- fancy2 -- **/ // hack for User:MacGyverMagic copyStructure('fancy', 'fancy2'); pg.structures.fancy2.popupTopLinks = function (x) { // hack out the at the end and put one at the beginning return ' ' + pg.structures.fancy.popupTopLinks(x).replace(RegExp(' $', 'i'), ''); };	pg.structures.fancy2.popupLayout = function { // move toplinks to after the title return [ 'popupError', 'popupImage', 'popupTitle', 'popupUserData', 'popupData', 'popupTopLinks', 'popupOtherLinks', 'popupRedir', [				'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ],			'popupMiscTools', ['popupRedlink'], 'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab', ];	};

/** -- menus -- **/ copyStructure('original', 'menus'); pg.structures.menus.popupLayout = function { return [ 'popupError', 'popupImage', 'popupTopLinks', 'popupTitle', 'popupOtherLinks', 'popupRedir', [				'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ],			'popupUserData', 'popupData', 'popupMiscTools', ['popupRedlink'], 'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab', ];	};

pg.structures.menus.popupTopLinks = function (x, shorter) { // FIXME maybe this stuff should be cached var s = []; var dropdiv = '<div class="popup_drop">'; var enddiv = ' '; var hist = '<<history|shortcut=h>>'; if (!shorter) { hist = ' ' + hist + '|<<historyfeed|rss>>|<<editors|shortcut=E>> '; }		var lastedit = '<<lastEdit|shortcut=/|show last edit>>'; var thank = 'if(diff){<<thank|send thanks>>}'; var jsHistory = '<<lastContrib|last set of edits>><<sinceMe|changes since mine>>'; var linkshere = '<<whatLinksHere|shortcut=l|what links here>>'; var related = '<<relatedChanges|shortcut=r|related changes>>'; var search = ' <<search|shortcut=s>>if(wikimedia){|<<globalsearch|shortcut=g|global>>}' + '|<<google|shortcut=G|web>> '; var watch = ' <<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>> '; var protect = ' <<unprotect|unprotectShort>>|' + '<<protect|shortcut=p>>|<<protectlog|log>> '; var del = ' <<undelete|undeleteShort>>|<<delete|shortcut=d>>|' + '<<deletelog|log>> '; var move = '<<move|shortcut=m|move page>>'; var nullPurge = ' <<nullEdit|shortcut=n|null edit>>|<<purge|shortcut=P>> '; var viewOptions = ' <<view|shortcut=v>>|<<render|shortcut=S>>|< > '; var editRow = 'if(oldid){' + ' <<edit|shortcut=e>>|<<editOld|shortcut=e|this revision>> ' + ' <<revert|shortcut=v>>|< > ' + '}else{<<edit|shortcut=e>>}'; var markPatrolled = 'if(rcid){<<markpatrolled|mark patrolled>>}'; var newTopic = 'if(talk){<<new|shortcut=+|new topic>>}'; var protectDelete = 'if(admin){' + protect + del + '}';

if (getValueOf('popupActionsMenu')) { s.push('< >*' + dropdiv + menuTitle('actions')); } else { s.push(dropdiv + '< >'); }		s.push(' '); s.push(editRow + markPatrolled + newTopic + hist + lastedit + thank); if (!shorter) { s.push(jsHistory); }		s.push(move + linkshere + related); if (!shorter) { s.push(nullPurge + search); }		if (!shorter) { s.push(viewOptions); }		s.push(' ' + watch + protectDelete); s.push(			' ' +				'if(talk){<<article|shortcut=a|view article>><<editArticle|edit article>>}' +				'else{<<talk|shortcut=t|talk page>><<editTalk|edit talk>>' +				'<<newTalk|shortcut=+|new topic>>} ' +				enddiv		);

// user menu starts here var email = '<<email|shortcut=E|email user>>'; var contribs = 'if(wikimedia){ }<<contribs|shortcut=c|contributions>>if(wikimedia){ }' + 'if(admin){ <<deletedContribs>> }';

s.push('if(user){*' + dropdiv + menuTitle('user')); s.push(' '); s.push(' <<userPage|shortcut=u|user page>>|<<userSpace|space>> '); s.push(			'<<userTalk|shortcut=t|user talk>><<editUserTalk|edit user talk>>' +				'<<newUserTalk|shortcut=+|leave comment>>'		); if (!shorter) { s.push('if(ipuser){< >}else{' + email + '}'); } else { s.push('if(ipuser){}else{' + email + '}'); }		s.push(' ' + contribs + '<<userlog|shortcut=L|user log>>'); s.push('if(wikimedia){<<count|shortcut=#|edit counter>>}'); s.push(			'if(admin){ <<unblock|unblockShort>>|<<block|shortcut=b|block user>> }'		); s.push('<<blocklog|shortcut=B|block log>>'); s.push(' ' + enddiv + '}');

// popups menu starts here if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) { x.navpop.hasPopupMenu = true; s.push('*' + dropdiv + menuTitle('popupsMenu') + ' '); s.push('<<togglePreviews|toggle previews>>'); s.push('<<purgePopups|reset>>'); s.push('<<disablePopups|disable>>'); s.push(' ' + enddiv); }		return navlinkStringToHTML(s.join(''), x.article, x.params); };

function menuTitle(s) { return '<a href="#" noPopup=1>' + popupString(s) + '</a>'; }

pg.structures.menus.popupRedirTitle = pg.structures.menus.popupTitle; pg.structures.menus.popupRedirTopLinks = pg.structures.menus.popupTopLinks;

copyStructure('menus', 'shortmenus'); pg.structures.shortmenus.popupTopLinks = function (x) { return pg.structures.menus.popupTopLinks(x, true); };	pg.structures.shortmenus.popupRedirTopLinks = pg.structures.shortmenus.popupTopLinks;

//</NOLITE> pg.structures.lite = {}; pg.structures.lite.popupLayout = function { return ['popupTitle', 'popupPreview']; };	pg.structures.lite.popupTitle = function (x) { log(x.article + ': structures.lite.popupTitle'); //return navlinkStringToHTML('< >',x.article,x.params); return ' <span class="popup_mainlink">' + x.article.toString + ' '; };	// ENDFILE: structures.js

// STARTFILE: autoedit.js	//<NOLITE> function substitute(data, cmdBody) { // alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags); var fromRe = RegExp(cmdBody.from, cmdBody.flags); return data.replace(fromRe, cmdBody.to); }

function execCmds(data, cmdList) { for (var i = 0; i < cmdList.length; ++i) { data = cmdList[i].action(data, cmdList[i]); }		return data; }

function parseCmd(str) { // returns a list of commands if (!str.length) { return []; }		var p = false; switch (str.charAt(0)) { case 's': p = parseSubstitute(str); break; default: return false; }		if (p) { return [p].concat(parseCmd(p.remainder)); }		return false; }

// FIXME: Only used once here, confusing with native (and more widely-used) unescape, should probably be replaced // Then again, unescape is semi-soft-deprecated, so we should look into replacing that too function unEscape(str, sep) { return str .split('\\\\') .join('\\') .split('\\' + sep) .join(sep) .split('\\n') .join('\n'); }

function parseSubstitute(str) { // takes a string like s/a/b/flags;othercmds and parses it

var from, to, flags, tmp;

if (str.length < 4) { return false; }		var sep = str.charAt(1); str = str.substring(2);

tmp = skipOver(str, sep); if (tmp) { from = tmp.segment; str = tmp.remainder; } else { return false; }

tmp = skipOver(str, sep); if (tmp) { to = tmp.segment; str = tmp.remainder; } else { return false; }

flags = ''; if (str.length) { tmp = skipOver(str, ';') || skipToEnd(str, ';'); if (tmp) { flags = tmp.segment; str = tmp.remainder; }		}

return { action: substitute, from: from, to: to, flags: flags, remainder: str, };	}

function skipOver(str, sep) { var endSegment = findNext(str, sep); if (endSegment < 0) { return false; }		var segment = unEscape(str.substring(0, endSegment), sep); return { segment: segment, remainder: str.substring(endSegment + 1) }; }

/*eslint-disable*/ function skipToEnd(str, sep) { return { segment: str, remainder: '' }; }	/*eslint-enable */

function findNext(str, ch) { for (var i = 0; i < str.length; ++i) { if (str.charAt(i) == '\\') { i += 2; }			if (str.charAt(i) == ch) { return i;			} }		return -1; }

function setCheckbox(param, box) { var val = mw.util.getParamValue(param); if (val) { switch (val) { case '1': case 'yes': case 'true': box.checked = true; break; case '0': case 'no': case 'false': box.checked = false; }		}	}

function autoEdit { setupPopups(function {			if (mw.util.getParamValue('autoimpl') !== popupString('autoedit_version')) {				return false;			}			if ( mw.util.getParamValue('autowatchlist') && mw.util.getParamValue('actoken') === autoClickToken ) {				pg.fn.modifyWatchlist(mw.util.getParamValue('title'), mw.util.getParamValue('action'));			}			if (!document.editform) {				return false;			}			if (autoEdit.alreadyRan) {				return false;			}			autoEdit.alreadyRan = true;			var cmdString = mw.util.getParamValue('autoedit');			if (cmdString) {				try {					var editbox = document.editform.wpTextbox1;					var cmdList = parseCmd(cmdString);					var input = editbox.value;					var output = execCmds(input, cmdList);					editbox.value = output;				} catch (dang) {					return;				}				// wikEd user script compatibility				if (typeof wikEdUseWikEd != 'undefined') {					if (wikEdUseWikEd === true) {						WikEdUpdateFrame;					}				}			}			setCheckbox('autominor', document.editform.wpMinoredit);			setCheckbox('autowatch', document.editform.wpWatchthis);

var rvid = mw.util.getParamValue('autorv'); if (rvid) { var url = pg.wiki.apiwikibase + '?action=query&format=json&formatversion=2&prop=revisions&revids=' + rvid; startDownload(url, null, autoEdit2); } else { autoEdit2; }		});	}

function autoEdit2(d) { var summary = mw.util.getParamValue('autosummary'); var summaryprompt = mw.util.getParamValue('autosummaryprompt'); var summarynotice = ''; if (d && d.data && mw.util.getParamValue('autorv')) { var s = getRvSummary(summary, d.data); if (s === false) { summaryprompt = true; summarynotice = popupString(					'Failed to get revision information, please edit manually.\n\n'				); summary = simplePrintf(summary, [					mw.util.getParamValue('autorv'),					'(unknown)',					'(unknown)',				]); } else { summary = s;			} }		if (summaryprompt) { var txt = summarynotice + popupString('Enter a non-empty edit summary or press cancel to abort'); var response = prompt(txt, summary); if (response) { summary = response; } else { return; }		}		if (summary) { document.editform.wpSummary.value = summary; }		// Attempt to avoid possible premature clicking of the save button // (maybe delays in updates to the DOM are to blame?? or a red herring) setTimeout(autoEdit3, 100); }

function autoClickToken { return mw.user.sessionId; }

function autoEdit3 { if (mw.util.getParamValue('actoken') != autoClickToken) { return; }

var btn = mw.util.getParamValue('autoclick'); if (btn) { if (document.editform && document.editform[btn]) { var button = document.editform[btn]; var msg = tprintf(					'The %s button has been automatically clicked. Please wait for the next page to load.',					[button.value]				); bannerMessage(msg); document.title = '(' + document.title + ')'; button.click; } else { alert(					tprintf('Could not find button %s. Please check the settings in your javascript file.', [ btn, ])				);			}		}	}

function bannerMessage(s) { var headings = document.getElementsByTagName('h1'); if (headings) { var div = document.createElement('div'); div.innerHTML = '<font size=+1>' + pg.escapeQuotesHTML(s) + ' '; headings[0].parentNode.insertBefore(div, headings[0]); }	}

function getRvSummary(template, json) { try { var o = getJsObj(json); var edit = anyChild(o.query.pages).revisions[0]; var timestamp = edit.timestamp .split(/[A-Z]/g) .join(' ') .replace(/^ *| *$/g, ''); return simplePrintf(template, [				edit.revid,				timestamp,				edit.userhidden ? '(hidden)' : edit.user,			]); } catch (badness) { return false; }	}

//</NOLITE> // ENDFILE: autoedit.js

// STARTFILE: downloader.js	/** * @fileoverview * {@link Downloader}, a xmlhttprequest wrapper, and helper functions. */

/**	 * Creates a new Downloader * @constructor * @class The Downloader class. Create a new instance of this class to download stuff. * @param {String} url The url to download. This can be omitted and supplied later. */	function Downloader(url) { if (typeof XMLHttpRequest != 'undefined') { this.http = new XMLHttpRequest; }

/**		 * The url to download * @type String */		this.url = url;

/**		 * A universally unique ID number * @type integer */		this.id = null;

/**		 * Modification date, to be culled from the incoming headers * @type Date * @private */		this.lastModified = null;

/**		 * What to do when the download completes successfully * @type Function * @private */		this.callbackFunction = null;

/**		 * What to do on failure * @type Function * @private */		this.onFailure = null;

/**		 * Flag set on 		 * @type boolean */		this.aborted = false;

/**		 * HTTP method. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html for * possibilities. * @type String */		this.method = 'GET'; /**		Async flag. @type boolean */		this.async = true; }

new Downloader;

/** Submits the http request. */	Downloader.prototype.send = function (x) { if (!this.http) { return null; }		return this.http.send(x); };

/** Aborts the download, setting the  field to true. */	Downloader.prototype.abort = function { if (!this.http) { return null; }		this.aborted = true; return this.http.abort; };

/** Returns the downloaded data. */	Downloader.prototype.getData = function { if (!this.http) { return null; }		return this.http.responseText; };

/** Prepares the download. */	Downloader.prototype.setTarget = function { if (!this.http) { return null; }		this.http.open(this.method, this.url, this.async); this.http.setRequestHeader('Api-User-Agent', pg.api.userAgent); };

/** Gets the state of the download. */	Downloader.prototype.getReadyState = function { if (!this.http) { return null; }		return this.http.readyState; };

pg.misc.downloadsInProgress = {};

/**	 * Starts the download. * Note that setTarget {@link Downloader#setTarget} must be run first */	Downloader.prototype.start = function { if (!this.http) { return; }		pg.misc.downloadsInProgress[this.id] = this; this.http.send(null); };

/**	 * Gets the 'Last-Modified' date from the download headers. * Should be run after the download completes. * Returns  on failure. * @return {Date} */	Downloader.prototype.getLastModifiedDate = function { if (!this.http) { return null; }		var lastmod = null; try { lastmod = this.http.getResponseHeader('Last-Modified'); } catch (err) {} if (lastmod) { return new Date(lastmod); }		return null; };

/**	 * Sets the callback function. * @param {Function} f callback function, called as  on success */	Downloader.prototype.setCallback = function (f) { if (!this.http) { return; }		this.http.onreadystatechange = f;	};

Downloader.prototype.getStatus = function { if (!this.http) { return null; }		return this.http.status; };

//////////////////////////////////////////////////	// helper functions

/**	 * Creates a new {@link Downloader} and prepares it for action. * @param {String} url The url to download * @param {integer} id The ID of the {@link Downloader} object * @param {Function} callback The callback function invoked on success * @return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser */	function newDownload(url, id, callback, onfailure) { var d = new Downloader(url); if (!d.http) { return 'ohdear'; }		d.id = id; d.setTarget; if (!onfailure) { onfailure = 2; }		var f = function { if (d.getReadyState == 4) { delete pg.misc.downloadsInProgress[this.id]; try { if (d.getStatus == 200) { d.data = d.getData; d.lastModified = d.getLastModifiedDate; callback(d); } else if (typeof onfailure == typeof 1) { if (onfailure > 0) { // retry newDownload(url, id, callback, onfailure - 1); }					} else if (typeof onfailure === 'function') { onfailure(d, url, id, callback); }				} catch (somerr) { /* ignore it */ }			}		};		d.setCallback(f); return d;	} /**	 * Simulates a download from cached data. * The supplied data is put into a {@link Downloader} as if it had downloaded it. * @param {String} url The url. * @param {integer} id The ID. * @param {Function} callback The callback, which is invoked immediately as , * where  is the new {@link Downloader}. * @param {String} data The (cached) data. * @param {Date} lastModified The (cached) last modified date. */	function fakeDownload(url, id, callback, data, lastModified, owner) { var d = newDownload(url, callback); d.owner = owner; d.id = id; d.data = data; d.lastModified = lastModified; return callback(d); }

/**	 * Starts a download. * @param {String} url The url to download * @param {integer} id The ID of the {@link Downloader} object * @param {Function} callback The callback function invoked on success * @return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser */	function startDownload(url, id, callback) { var d = newDownload(url, id, callback); if (typeof d == typeof '') { return d;		} d.start; return d;	}

/**	 * Aborts all downloads which have been started. */	function abortAllDownloads { for (var x in pg.misc.downloadsInProgress) { try { pg.misc.downloadsInProgress[x].aborted = true; pg.misc.downloadsInProgress[x].abort; delete pg.misc.downloadsInProgress[x]; } catch (e) {} }	}	// ENDFILE: downloader.js

// STARTFILE: livepreview.js	// TODO: location is often not correct (eg relative links in previews) // NOTE: removed md5 and image and math parsing. was broken, lots of bytes. /**	 * InstaView - a Mediawiki to HTML converter in JavaScript * Version 0.6.1 * Copyright (C) Pedro Fayolle 2005-2006 * https://en.wikipedia.org/wiki/User:Pilaf * Distributed under the BSD license *	 * Changelog: *	 * 0.6.1	 * - Fixed problem caused by \r characters * - Improved inline formatting parser *	 * 0.6	 * - Changed name to InstaView * - Some major code reorganizations and factored out some common functions * - Handled conversion of relative links (i.e. /foo) * - Fixed misrendering of adjacent definition list items * - Fixed bug in table headings handling * - Changed date format in signatures to reflect Mediawiki's	 * - Fixed handling of Image:... * - Updated MD5 function (hopefully it will work with UTF-8) * - Fixed bug in handling of links inside images *	 * To do: * - Better support for math tags * - Full support for * - Parser-based (as opposed to RegExp-based) inline wikicode handling (make it one-pass and	 *  bullet-proof) * - Support for templates (through AJAX) * - Support for coloured links (AJAX) */

var Insta = {};

function setupLivePreview { // options Insta.conf = { baseUrl: '',

user: {},

wiki: { lang: pg.wiki.lang, interwiki: pg.wiki.interwiki, default_thumb_width: 180, },

paths: { articles: pg.wiki.articlePath + '/', // Only used for Insta previews with images. (not in popups) math: '/math/', images: '//upload.wikimedia.org/wikipedia/en/', // FIXME getImageUrlStart(pg.wiki.hostname), images_fallback: '//upload.wikimedia.org/wikipedia/commons/', },

locale: { user: mw.config.get('wgFormattedNamespaces')[pg.nsUserId], image: mw.config.get('wgFormattedNamespaces')[pg.nsImageId], category: mw.config.get('wgFormattedNamespaces')[pg.nsCategoryId], // shouldn't be used in popup previews, i think months: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ],			},		};

// options with default values or backreferences Insta.conf.user.name = Insta.conf.user.name || 'Wikipedian'; Insta.conf.user.signature =  +			Insta.conf.user.name +			; //Insta.conf.paths.images = '//upload.wikimedia.org/wikipedia/' + Insta.conf.wiki.lang + '/';

// define constants Insta.BLOCK_IMAGE = new RegExp(			'^\\[\\[(?:File|Image|' + Insta.conf.locale.image + '):.*?\\|.*?(?:frame|thumbnail|thumb|none|right|left|center)',			'i'		); }

Insta.dump = function (from, to) { if (typeof from == 'string') { from = document.getElementById(from); }		if (typeof to == 'string') { to = document.getElementById(to); }		to.innerHTML = this.convert(from.value); };

Insta.convert = function (wiki) { var ll = typeof wiki == 'string' ? wiki.replace(/\r/g, '').split(/\n/) : wiki, // lines of wikicode o = '', // output p = 0, // para flag r; // result of passing a regexp to compareLineStringOrReg

// some shorthands function remain { return ll.length; }		function sh { return ll.shift; } // shift function ps(s) { o += s;		} // push

// similar to C's printf, uses ? as placeholders, ?? to escape question marks function f { var i = 1, a = arguments, f = a[0], o = '', c, p; for (i < a.length; i++) { if ((p = f.indexOf('?')) + 1) { // allow character escaping i -= c = f.charAt(p + 1) == '?' ? 1 : 0;					o += f.substring(0, p) + (c ? '?' : a[i]); f = f.substr(p + 1 + c); } else { break; }			}			return o + f;		}

function html_entities(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

// Wiki text parsing to html is a nightmare. // The below functions deliberately don't escape the ampersand since this would make it more // difficult, and we don't absolutely need to for how we need it. This means that any // unescaped ampersands in wikitext will remain unescaped and can cause invalid HTML. // Browsers should all be able to handle it though. We also escape significant wikimarkup // characters to prevent further matching on the processed text. function htmlescape_text(s) { return s				.replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/:/g, '&#58;') .replace(/\[/g, '&#91;') .replace(/]/g, '&#93;'); }		function htmlescape_attr(s) { return htmlescape_text(s).replace(/'/g, '&#39;').replace(/"/g, '&quot;');		}

// return the first non matching character position between two strings function str_imatch(a, b) { for (var i = 0, l = Math.min(a.length, b.length); i < l; i++) { if (a.charAt(i) != b.charAt(i)) { break; }			}			return i;		}

// compare current line against a string or regexp // if passed a string it will compare only the first string.length characters // if passed a regexp the result is stored in r		function compareLineStringOrReg(c) { return typeof c == 'string' ? ll[0] && ll[0].substr(0, c.length) == c				: (r = ll[0] && ll[0].match(c)); }

function compareLineString(c) { return ll[0] == c;		} // compare current line against a string function charAtPoint(p) { return ll[0].charAt(p); } // return char at pos p

function endl(s) { ps(s); sh; }

function parse_list { var prev = '';

while (remain && compareLineStringOrReg(/^([*#:;]+)(.*)$/)) { var l_match = r;

sh;

var ipos = str_imatch(prev, l_match[1]);

// close uncontinued lists for (var prevPos = prev.length - 1; prevPos >= ipos; prevPos--) { var pi = prev.charAt(prevPos);

if (pi == '*') { ps('</ul>'); } else if (pi == '#') { ps('</ol>'); }					// close a dl only if the new item is not a dl item (:, ; or empty) else if ($.inArray(l_match[1].charAt(prevPos), ['', '*', '#'])) { ps('</dl>'); }				}

// open new lists for (var matchPos = ipos; matchPos < l_match[1].length; matchPos++) { var li = l_match[1].charAt(matchPos);

if (li == '*') { ps('<ul>'); } else if (li == '#') { ps('<ol>'); }					// open a new dl only if the prev item is not a dl item (:, ; or empty) else if ($.inArray(prev.charAt(matchPos), ['', '*', '#'])) { ps('<dl>'); }				}

switch (l_match[1].charAt(l_match[1].length - 1)) { case '*': case '#': ps('<li>' + parse_inline_nowiki(l_match[2])); break;

case ';': ps('<dt>');

var dt_match = l_match[2].match(/(.*?)(:.*?)$/);

// handle ;dt :dd format if (dt_match) { ps(parse_inline_nowiki(dt_match[1])); ll.unshift(dt_match[2]); } else ps(parse_inline_nowiki(l_match[2])); break;

case ':': ps('<dd>' + parse_inline_nowiki(l_match[2])); }

prev = l_match[1]; }

// close remaining lists for (var i = prev.length - 1; i >= 0; i--) { ps(f('</?>', prev.charAt(i) == '*' ? 'ul' : prev.charAt(i) == '#' ? 'ol' : 'dl')); }		}

function parse_table { endl(f(' ');							return;						case '-':							endl(f(' ', compareLineStringOrReg(/\|-*(.*)/)[1]));							break;						default:							parse_table_data;					}				else if (compareLineStringOrReg('!')) {					parse_table_data;				} else {					sh;				}		}

function parse_table_data { var td_line, match_i;

// 1: "|+", '|' or '+' // 2: ??			// 3: attributes ?? // TODO: finish commenting this regexp var td_match = sh.match(/^(\|\+|\||!)((?:([^[|]*?)\|(?!\|))?(.*))$/);

if (td_match[1] == '|+') ps('<caption'); else ps('<t' + (td_match[1] == '|' ? 'd' : 'h'));

if (typeof td_match[3] != 'undefined') { //ps(' ' + td_match[3]) match_i = 4; } else match_i = 2;

ps('>');

if (td_match[1] != '|+') { // use || or !! as a cell separator depending on context // NOTE: when split is passed a regexp make sure to use non-capturing brackets td_line = td_match[match_i].split(td_match[1] == '|' ? '||' : /(?:\|\||!!)/);

ps(parse_inline_nowiki(td_line.shift));

while (td_line.length) ll.unshift(td_match[1] + td_line.pop); } else { ps(parse_inline_nowiki(td_match[match_i])); }

var tc = 0, td = [];

while (remain) { td.push(sh); if (compareLineStringOrReg('|')) { if (!tc) break; // we're at the outer-most level (no nested tables), skip to td parse else if (charAtPoint(1) == '}') tc--; } else if (!tc && compareLineStringOrReg('!')) break; else if (compareLineStringOrReg('{|')) tc++; }

if (td.length) ps(Insta.convert(td)); }

function parse_pre { ps(' '); do { endl(parse_inline_nowiki(ll[0].substring(1)) + '\n'); } while (remain && compareLineStringOrReg(' ')); ps(' '); }

function parse_block_image { ps(parse_image(sh)); }

function parse_image(str) { //<NOLITE> // get what's in between "" var tag = str.substring(str.indexOf(':') + 1, str.length - 2); /* eslint-disable no-unused-vars */ var width; var attr = [], filename, caption = ''; var thumb = 0, frame = 0, center = 0; var align = ''; /* eslint-enable no-unused-vars */

if (tag.match(/\|/)) { // manage nested links var nesting = 0; var last_attr; for (var i = tag.length - 1; i > 0; i--) { if (tag.charAt(i) == '|' && !nesting) { last_attr = tag.substr(i + 1); tag = tag.substring(0, i); break; } else switch (tag.substr(i - 1, 2)) { case ']]': nesting++; i--; break; case '[[':								nesting--;								i--;						}				}

attr = tag.split(/\s*\|\s*/); attr.push(last_attr); filename = attr.shift;

var w_match;

for (attr.length; attr.shift) { w_match = attr[0].match(/^(\d*)(?:[px]*\d*)?px$/); if (w_match) width = w_match[1]; else switch (attr[0]) { case 'thumb': case 'thumbnail': thumb = true; frame = true; break; case 'frame': frame = true; break; case 'none': case 'right': case 'left': center = false; align = attr[0]; break; case 'center': center = true; align = 'none'; break; default: if (attr.length == 1) caption = attr[0]; }				}			} else filename = tag;

return ''; //</NOLITE> }

function parse_inline_nowiki(str) { var start, lastend = 0; var substart = 0, nestlev = 0, open, close, subloop; var html = '';

while (-1 != (start = str.indexOf(' ', substart))) { html += parse_inline_wiki(str.substring(lastend, start)); start += 8; substart = start; subloop = true; do { open = str.indexOf(' ', substart); close = str.indexOf(' ', substart); if (close <= open || open == -1) { if (close == -1) { return html + html_entities(str.substr(start)); }						substart = close + 9; if (nestlev) { nestlev--; } else { lastend = substart; html += html_entities(str.substring(start, lastend - 9)); subloop = false; }					} else { substart = open + 8; nestlev++; }				} while (subloop); }

return html + parse_inline_wiki(str.substr(lastend)); }

function parse_inline_images(str) { //<NOLITE> var start, substart = 0, nestlev = 0; var loop, close, open, wiki, html;

while (-1 != (start = str.indexOf('File|' + Insta.conf.locale.image + '):', 'i'))				) {					loop = true;					substart = start;					do {						substart += 2;						close = str.indexOf('', substart); open = str.indexOf('[[', substart);						if (close <= open || open == -1) {							if (close == -1) return str;							substart = close;							if (nestlev) {								nestlev--;							} else {								wiki = str.substring(start, close + 2);								html = parse_image(wiki);								str = str.replace(wiki, html);								substart = start + html.length;								loop = false;							}						} else {							substart = open;							nestlev++;						}					} while (loop);				} else break;			}

//</NOLITE> return str; }

// the output of this function doesn't respect the FILO structure of HTML // but since most browsers can handle it I'll save myself the hassle function parse_inline_formatting(str) { var em, st, i, li, o = ''; while ((i = str.indexOf("''", li)) + 1) { o += str.substring(li, i); li = i + 2; if (str.charAt(i + 2) == "'") { li++; st = !st; o += st ? ' ' : ' ';				} else { em = !em; o += em ? ' ' : ' ';				}			}			return o + str.substr(li); }

function parse_inline_wiki(str) { str = parse_inline_images(str); str = parse_inline_formatting(str);

// math str = str.replace(/<(?:)math>(.*?)<\/math>/gi, '');

// Build a Mediawiki-formatted date string var date = new Date; var minutes = date.getUTCMinutes; if (minutes < 10) minutes = '0' + minutes; date = f(				'?:?, ? ? ? (UTC)',				date.getUTCHours,				minutes,				date.getUTCDate,				Insta.conf.locale.months[date.getUTCMonth],				date.getUTCFullYear			);

// text formatting return (				str					// signatures					.replace(/~{5}(?!~)/g, date)					.replace(/~{4}(?!~)/g, Insta.conf.user.name + ' ' + date)					.replace(/~{3}(?!~)/g, Insta.conf.user.name)					// Category:..., Image:..., etc...					.replace( RegExp(							'\\[\\[:((?:' +								Insta.conf.locale.category +								'|Image|File|' +								Insta.conf.locale.image +								'|' +								Insta.conf.wiki.interwiki +								'):[^|]*?)\\]\\](\\w*)',							'gi'						), function ($0, $1, $2) { return f(								"<a href='?'>?</a>",								Insta.conf.paths.articles + htmlescape_attr($1),								htmlescape_text($1) + htmlescape_text($2)							); }					)					// remove straight category and interwiki tags					.replace( RegExp(							'\\[\\[(?:' + Insta.conf.locale.category + '|' +								Insta.conf.wiki.interwiki + '):.*?\\]\\]',							'gi'						), ''					)					// Links, Links, etc...					.replace( RegExp(							'\\[\\[:((?:' +								Insta.conf.locale.category +								'|Image|File|' +								Insta.conf.locale.image +								'|' +								Insta.conf.wiki.interwiki +								'):.*?)\\|([^\\]]+?)\\]\\](\\w*)',							'gi'						), function ($0, $1, $2, $3) { return f(								"<a href='?'>?</a>",								Insta.conf.paths.articles + htmlescape_attr($1),								htmlescape_text($2) + htmlescape_text($3)							); }					)					// /Relative links					.replace(/\[\[(\/[^|]*?)\]\]/g, function ($0, $1) { return f(							"<a href='?'>?</a>",							Insta.conf.baseUrl + htmlescape_attr($1),							htmlescape_text($1)						); })					// Relative links					.replace(/\[\[(\/.*?)\|(.+?)\]\]/g, function ($0, $1, $2) { return f(							"<a href='?'>?</a>",							Insta.conf.baseUrl + htmlescape_attr($1),							htmlescape_text($2)						); })					// Common links					.replace(/\[\[([^[|]*?)\]\](\w*)/g, function ($0, $1, $2) { return f(							"<a href='?'>?</a>",							Insta.conf.paths.articles + htmlescape_attr($1),							htmlescape_text($1) + htmlescape_text($2)						); })					// Links					.replace(/\[\[([^[]*?)\|([^\]]+?)\]\](\w*)/g, function ($0, $1, $2, $3) { return f(							"<a href='?'>?</a>",							Insta.conf.paths.articles + htmlescape_attr($1),							htmlescape_text($2) + htmlescape_text($3)						); })					// Namespace					.replace(/\[\[([^\]]*?:)?(.*?)( *\(.*?\))?\|\]\]/g, function ($0, $1, $2, $3) { return f(							"<a href='?'>?</a>",							Insta.conf.paths.articles +								htmlescape_attr($1) +								htmlescape_attr($2) +								htmlescape_attr($3),							htmlescape_text($2)						); })					// External links					.replace( /\[(https?|news|ftp|mailto|gopher|irc):(\/*)([^\]]*?) (.*?)\]/g, function ($0, $1, $2, $3, $4) { return f(								"<a class='external' href='?:?'>?</a>",								htmlescape_attr($1),								htmlescape_attr($2) + htmlescape_attr($3),								htmlescape_text($4)							); }					)					.replace(/\[http:\/\/(.*?)\]/g, function ($0, $1) { return f("<a class='external' href='http://?'>[#]</a>", htmlescape_attr($1)); })					.replace(/\[(news|ftp|mailto|gopher|irc):(\/*)(.*?)\]/g, function ($0, $1, $2, $3) { return f(							"<a class='external' href='?:?'>?:?</a>",							htmlescape_attr($1),							htmlescape_attr($2) + htmlescape_attr($3),							htmlescape_text($1),							htmlescape_text($2) + htmlescape_text($3)						); })					.replace( /(^| )(https?|news|ftp|mailto|gopher|irc):(\/*)([^ $]*[^.,!?;: $])/g, function ($0, $1, $2, $3, $4) { return f(								"?<a class='external' href='?:?'>?:?</a>",								htmlescape_text($1),								htmlescape_attr($2),								htmlescape_attr($3) + htmlescape_attr($4),								htmlescape_text($2),								htmlescape_text($3) + htmlescape_text($4)							); }					)					.replace(, )					.replace('__NOINDEX__', )					.replace('__INDEX__', )					.replace(, )			); }

// begin parsing for (remain; ) if (compareLineStringOrReg(/^(={1,6})(.*)\1(.*)$/)) { p = 0; endl(f('<h?>?</h?>?', r[1].length, parse_inline_nowiki(r[2]), r[1].length, r[3])); } else if (compareLineStringOrReg(/^[*#:;]/)) { p = 0; parse_list; } else if (compareLineStringOrReg(' ')) { p = 0; parse_pre; } else if (compareLineStringOrReg('{|')) { p = 0; parse_table; } else if (compareLineStringOrReg(/^+$/)) { p = 0; endl(' '); } else if (compareLineStringOrReg(Insta.BLOCK_IMAGE)) { p = 0; parse_block_image; } else { // handle paragraphs if (compareLineString('')) { p = remain > 1 && ll[1] === ''; if (p) endl(' '); } else { if (!p) { ps(' '); p = 1; }					ps(parse_inline_nowiki(ll[0]) + ' '); }

sh; }

return o;	};

function wiki2html(txt, baseurl) { Insta.conf.baseUrl = baseurl; return Insta.convert(txt); }	// ENDFILE: livepreview.js

// STARTFILE: pageinfo.js	//<NOLITE> function popupFilterPageSize(data) { return formatBytes(data.length); }

function popupFilterCountLinks(data) { var num = countLinks(data); return String(num) + ' ' + (num != 1 ? popupString('wikiLinks') : popupString('wikiLink')); }

function popupFilterCountImages(data) { var num = countImages(data); return String(num) + ' ' + (num != 1 ? popupString('images') : popupString('image')); }

function popupFilterCountCategories(data) { var num = countCategories(data); return (			String(num) + ' ' + (num != 1 ? popupString('categories') : popupString('category'))		); }

function popupFilterLastModified(data, download) { var lastmod = download.lastModified; var now = new Date; var age = now - lastmod; if (lastmod && getValueOf('popupLastModified')) { return tprintf('%s old', [formatAge(age)]).replace(RegExp(' ', 'g'), ' '); }		return ''; }

function formatAge(age) { // coerce into a number var a = 0 + age, aa = a;

var seclen = 1000; var minlen = 60 * seclen; var hourlen = 60 * minlen; var daylen = 24 * hourlen; var weeklen = 7 * daylen;

var numweeks = (a - (a % weeklen)) / weeklen; a = a - numweeks * weeklen; var sweeks = addunit(numweeks, 'week'); var numdays = (a - (a % daylen)) / daylen; a = a - numdays * daylen; var sdays = addunit(numdays, 'day'); var numhours = (a - (a % hourlen)) / hourlen; a = a - numhours * hourlen; var shours = addunit(numhours, 'hour'); var nummins = (a - (a % minlen)) / minlen; a = a - nummins * minlen; var smins = addunit(nummins, 'minute'); var numsecs = (a - (a % seclen)) / seclen; a = a - numsecs * seclen; var ssecs = addunit(numsecs, 'second');

if (aa > 4 * weeklen) { return sweeks; }		if (aa > weeklen) { return sweeks + ' ' + sdays; }		if (aa > daylen) { return sdays + ' ' + shours; }		if (aa > 6 * hourlen) { return shours; }		if (aa > hourlen) { return shours + ' ' + smins; }		if (aa > 10 * minlen) { return smins; }		if (aa > minlen) { return smins + ' ' + ssecs; }		return ssecs; }

function addunit(num, str) { return '' + num + ' ' + (num != 1 ? popupString(str + 's') : popupString(str)); }

function runPopupFilters(list, data, download) { var ret = []; for (var i = 0; i < list.length; ++i) { if (list[i] && typeof list[i] == 'function') { var s = list[i](data, download, download.owner.article); if (s) { ret.push(s); }			}		}		return ret; }

function getPageInfo(data, download) { if (!data || data.length === 0) { return popupString('Empty page'); }

var popupFilters = getValueOf('popupFilters') || []; var extraPopupFilters = getValueOf('extraPopupFilters') || []; var pageInfoArray = runPopupFilters(popupFilters.concat(extraPopupFilters), data, download);

var pageInfo = pageInfoArray.join(', '); if (pageInfo !== '') { pageInfo = upcaseFirst(pageInfo); }		return pageInfo; }

// this could be improved! function countLinks(wikiText) { return wikiText.split('[[').length - 1;	}

// if N = # matches, n = # brackets, then // String.parenSplit(regex) intersperses the N+1 split elements // with Nn other elements. So total length is // L= N+1 + Nn = N(n+1)+1. So N=(L-1)/(n+1).

function countImages(wikiText) { return (wikiText.parenSplit(pg.re.image).length - 1) / (pg.re.imageBracketCount + 1); }

function countCategories(wikiText) { return (wikiText.parenSplit(pg.re.category).length - 1) / (pg.re.categoryBracketCount + 1); }

function popupFilterStubDetect(data, download, article) { var counts = stubCount(data, article); if (counts.real) { return popupString('stub'); }		if (counts.sect) { return popupString('section stub'); }		return ''; }

function popupFilterDisambigDetect(data, download, article) { if (!getValueOf('popupAllDabsStubs') && article.namespace) { return ''; }		return isDisambig(data, article) ? popupString('disambig') : ''; }

function formatBytes(num) { return num > 949 ? Math.round(num / 100) / 10 + popupString('kB') : num + ' ' + popupString('bytes'); }	//</NOLITE> // ENDFILE: pageinfo.js

// STARTFILE: titles.js	/** * @fileoverview Defines the {@link Title} class, and associated crufty functions.

*  deals with article titles and their various * forms. {@link Stringwrapper} is the parent class of	 *, which exists simply to make things a little * neater. */

/**	 * Creates a new Stringwrapper. * @constructor

* @class the Stringwrapper class. This base class is not really * useful on its own; it just wraps various common string operations. */	function Stringwrapper { /**		 * Wrapper for this.toString.indexOf * @param {String} x		 * @type integer */		this.indexOf = function (x) { return this.toString.indexOf(x); };		/**		 * Returns this.value. * @type String */		this.toString = function { return this.value; };		/**		 * Wrapper for {@link String#parenSplit} applied to this.toString * @param {RegExp} x		 * @type Array */		this.parenSplit = function (x) { return this.toString.parenSplit(x); };		/**		 * Wrapper for this.toString.substring * @param {String} x		 * @param {String} y (optional) * @type String */		this.substring = function (x, y) { if (typeof y == 'undefined') { return this.toString.substring(x); }			return this.toString.substring(x, y); };		/**		 * Wrapper for this.toString.split * @param {String} x		 * @type Array */		this.split = function (x) { return this.toString.split(x); };		/**		 * Wrapper for this.toString.replace * @param {String} x		 * @param {String} y		 * @type String */		this.replace = function (x, y) { return this.toString.replace(x, y); };	}

/**	 * Creates a new. * @constructor *	 * @class The Title class. Holds article titles and converts them into * various forms. Also deals with anchors, by which we mean the bits * of the article URL after a # character, representing locations * within an article. *	 * @param {String} value The initial value to assign to the * article. This must be the canonical title (see {@link	 * Title#value}. Omit this in the constructor and use another function	 * to set the title if this is unavailable.	 */	function Title(val) {		/**		 * The canonical article title. This must be in UTF-8 with no		 * entities, escaping or nasties. Also, underscores should be		 * replaced with spaces.		 * @type String		 * @private		 */		this.value = null;

/**		 * The canonical form of the anchor. This should be exactly as * it appears in the URL, i.e. with the .C3.0A bits in. * @type String */		this.anchor = '';

this.setUtf(val); }	Title.prototype = new Stringwrapper; /**	 * Returns the canonical representation of the article title, optionally without anchor. * @param {boolean} omitAnchor * @fixme Decide specs for anchor * @return String The article title and the anchor. */	Title.prototype.toString = function (omitAnchor) { return this.value + (!omitAnchor && this.anchor ? '#' + this.anchorString : ''); };	Title.prototype.anchorString = function { if (!this.anchor) { return ''; }		var split = this.anchor.parenSplit(/((?:[.][0-9A-F]{2})+)/); var len = split.length; var value; for (var j = 1; j < len; j += 2) { // FIXME s/decodeURI/decodeURIComponent/g ? value = split[j].split('.').join('%'); try { value = decodeURIComponent(value); } catch (e) { // cannot decode }			split[j] = value.split('_').join(' '); }		return split.join(''); };	Title.prototype.urlAnchor = function { var split = this.anchor.parenSplit('/((?:[%][0-9A-F]{2})+)/'); var len = split.length; for (var j = 1; j < len; j += 2) { split[j] = split[j].split('%').join('.'); }		return split.join(''); };	Title.prototype.anchorFromUtf = function (str) { this.anchor = encodeURIComponent(str.split(' ').join('_')) .split('%3A') .join(':') .split("'") .join('%27') .split('%') .join('.'); };	Title.fromURL = function (h) { return new Title.fromURL(h); };	Title.prototype.fromURL = function (h) { if (typeof h != 'string') { this.value = null; return this; }

// NOTE : playing with decodeURI, encodeURI, escape, unescape, // we seem to be able to replicate the IE borked encoding

// IE doesn't do this new-fangled utf-8 thing. // and it's worse than that. // IE seems to treat the query string differently to the rest of the url // the query is treated as bona-fide utf8, but the first bit of the url is pissed around with

// we fix up & for all browsers, just in case. var splitted = h.split('?'); splitted[0] = splitted[0].split('&').join('%26');

h = splitted.join('?');

var contribs = pg.re.contribs.exec(h); if (contribs) { if (contribs[1] == 'title=') { contribs[3] = contribs[3].split('+').join(' '); }			var u = new Title(contribs[3]); this.setUtf(				this.decodeNasties( mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + u.stripNamespace )			);			return this; }

var email = pg.re.email.exec(h); if (email) { this.setUtf(				this.decodeNasties( mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' +						new Title(email[3]).stripNamespace )			);			return this; }

var backlinks = pg.re.backlinks.exec(h); if (backlinks) { this.setUtf(this.decodeNasties(new Title(backlinks[3]))); return this; }

//A dummy title object for a Special:Diff link. var specialdiff = pg.re.specialdiff.exec(h); if (specialdiff) { this.setUtf(				this.decodeNasties( new Title(mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':Diff') )			);			return this; }

// no more special cases to check -- // hopefully it's not a disguised user-related or specially treated special page // Includes references var m = pg.re.main.exec(h); if (m === null) { this.value = null; } else { var fromBotInterface = /[?](.+[&])?title=/.test(h); if (fromBotInterface) { m[2] = m[2].split('+').join('_'); }			var extracted = m[2] + (m[3] ? '#' + m[3] : ''); if (pg.flag.isSafari && /%25[0-9A-Fa-f]{2}/.test(extracted)) { // Fix Safari issue // Safari sometimes encodes % as %25 in UTF-8 encoded strings like %E5%A3 -> %25E5%25A3. this.setUtf(decodeURIComponent(unescape(extracted))); } else { this.setUtf(this.decodeNasties(extracted)); }		}		return this; };	Title.prototype.decodeNasties = function (txt) { // myDecodeURI uses decodeExtras, which removes _, // thus ruining citations previews, which are formated as "cite_note-1" try { var ret = decodeURI(this.decodeEscapes(txt)); ret = ret.replace(/[_ ]*$/, ''); return ret; } catch (e) { return txt; // cannot decode }	};	// Decode valid %-encodings, otherwise escape them Title.prototype.decodeEscapes = function (txt) { var split = txt.parenSplit(/((?:[%][0-9A-Fa-f]{2})+)/); var len = split.length; // No %-encoded items found, so replace the literal % if (len === 1) { return split[0].replace(/%(?![0-9a-fA-F][0-9a-fA-F])/g, '%25'); }		for (var i = 1; i < len; i = i + 2) { split[i] = decodeURIComponent(split[i]); }		return split.join(''); };	Title.fromAnchor = function (a) { return new Title.fromAnchor(a); };	Title.prototype.fromAnchor = function (a) { if (!a) { this.value = null; return this; }		return this.fromURL(a.href); };	Title.fromWikiText = function (txt) { return new Title.fromWikiText(txt); };	Title.prototype.fromWikiText = function (txt) { // FIXME - testing needed txt = myDecodeURI(txt); this.setUtf(txt); return this; };	Title.prototype.hintValue = function { if (!this.value) { return ''; }		return safeDecodeURI(this.value); };	//<NOLITE> Title.prototype.toUserName = function (withNs) { if (this.namespaceId != pg.nsUserId && this.namespaceId != pg.nsUsertalkId) { this.value = null; return; }		this.value = (withNs ? mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' : '') + this.stripNamespace.split('/')[0]; };	Title.prototype.userName = function (withNs) { var t = new Title(this.value); t.toUserName(withNs); if (t.value) { return t;		} return null; };	Title.prototype.toTalkPage = function { // convert article to a talk page, or if we can't, return null // In other words: return null if this ALREADY IS a talk page // and return the corresponding talk page otherwise //		// Per https://www.mediawiki.org/wiki/Manual:Namespace#Subject_and_talk_namespaces // * All discussion namespaces have odd-integer indices // * The discussion namespace index for a specific namespace with index n is n + 1 if (this.value === null) { return null; }

var namespaceId = this.namespaceId; if (namespaceId >= 0 && namespaceId % 2 === 0) { //non-special and subject namespace var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId + 1]; if (typeof localizedNamespace !== 'undefined') { if (localizedNamespace === '') { this.value = this.stripNamespace; } else { this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace; }				return this.value; }		}

this.value = null; return null; };	//</NOLITE> // Return canonical, localized namespace Title.prototype.namespace = function { return mw.config.get('wgFormattedNamespaces')[this.namespaceId]; };	Title.prototype.namespaceId = function { var n = this.value.indexOf(':'); if (n < 0) { return 0; } //mainspace var namespaceId = mw.config.get('wgNamespaceIds')[ this.value.substring(0, n).split(' ').join('_').toLowerCase ];		if (typeof namespaceId == 'undefined') return 0; //mainspace return namespaceId; };	//<NOLITE> Title.prototype.talkPage = function { var t = new Title(this.value); t.toTalkPage; if (t.value) { return t;		} return null; };	Title.prototype.isTalkPage = function { if (this.talkPage === null) { return true; }		return false; };	Title.prototype.toArticleFromTalkPage = function { //largely copy/paste from toTalkPage above. if (this.value === null) { return null; }

var namespaceId = this.namespaceId; if (namespaceId >= 0 && namespaceId % 2 == 1) { //non-special and talk namespace var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId - 1]; if (typeof localizedNamespace !== 'undefined') { if (localizedNamespace === '') { this.value = this.stripNamespace; } else { this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace; }				return this.value; }		}

this.value = null; return null; };	Title.prototype.articleFromTalkPage = function { var t = new Title(this.value); t.toArticleFromTalkPage; if (t.value) { return t;		} return null; };	Title.prototype.articleFromTalkOrArticle = function { var t = new Title(this.value); if (t.toArticleFromTalkPage) { return t;		} return this; };	Title.prototype.isIpUser = function { return pg.re.ipUser.test(this.userName); };	//</NOLITE> Title.prototype.stripNamespace = function { // returns a string, not a Title var n = this.value.indexOf(':'); if (n < 0) { return this.value; }		var namespaceId = this.namespaceId; if (namespaceId === pg.nsMainspaceId) return this.value; return this.value.substring(n + 1); };	Title.prototype.setUtf = function (value) { if (!value) { this.value = ''; return; }		var anch = value.indexOf('#'); if (anch < 0) { this.value = value.split('_').join(' '); this.anchor = ''; return; }		this.value = value.substring(0, anch).split('_').join(' '); this.anchor = value.substring(anch + 1); this.ns = null; // wait until namespace is called };	Title.prototype.setUrl = function (urlfrag) { var anch = urlfrag.indexOf('#'); this.value = safeDecodeURI(urlfrag.substring(0, anch)); this.anchor = this.value.substring(anch + 1); };	Title.prototype.append = function (x) { this.setUtf(this.value + x); };	Title.prototype.urlString = function (x) { if (!x) { x = {}; }		var v = this.toString(true); if (!x.omitAnchor && this.anchor) { v += '#' + this.urlAnchor; }		if (!x.keepSpaces) { v = v.split(' ').join('_'); }		return encodeURI(v).split('&').join('%26').split('?').join('%3F').split('+').join('%2B'); };	Title.prototype.removeAnchor = function { return new Title(this.toString(true)); };	Title.prototype.toUrl = function { return pg.wiki.titlebase + this.urlString; };

function parseParams(url) { var specialDiff = pg.re.specialdiff.exec(url); if (specialDiff) { var split = specialDiff[1].split('/'); if (split.length == 1) return { oldid: split[0], diff: 'prev' }; else if (split.length == 2) return { oldid: split[0], diff: split[1] }; }

var ret = {}; if (url.indexOf('?') == -1) { return ret; }		url = url.split('#')[0]; var s = url.split('?').slice(1).join; var t = s.split('&'); for (var i = 0; i < t.length; ++i) { var z = t[i].split('='); z.push(null); ret[z[0]] = z[1]; }		//Diff revision with no oldid is interpreted as a diff to the previous revision by MediaWiki if (ret.diff && typeof ret.oldid === 'undefined') { ret.oldid = 'prev'; }		//Documentation seems to say something different, but oldid can also accept prev/next, and //Echo is emitting such URLs. Simple fixup during parameter decoding: if (ret.oldid && (ret.oldid === 'prev' || ret.oldid === 'next' || ret.oldid === 'cur')) { var helper = ret.diff; ret.diff = ret.oldid; ret.oldid = helper; }		return ret; }

// (a) myDecodeURI (first standard decodeURI, then pg.re.urlNoPopup) // (b) change spaces to underscores // (c) encodeURI (just the straight one, no pg.re.urlNoPopup)

function myDecodeURI(str) { var ret; // FIXME decodeURIComponent?? try { ret = decodeURI(str.toString); } catch (summat) { return str; }		for (var i = 0; i < pg.misc.decodeExtras.length; ++i) { var from = pg.misc.decodeExtras[i].from; var to = pg.misc.decodeExtras[i].to; ret = ret.split(from).join(to); }		return ret; }

function safeDecodeURI(str) { var ret = myDecodeURI(str); return ret || str; }

///////////	// TESTS // ///////////

//<NOLITE> function isDisambig(data, article) { if (!getValueOf('popupAllDabsStubs') && article.namespace) { return false; }		return !article.isTalkPage && pg.re.disambig.test(data); }

function stubCount(data, article) { if (!getValueOf('popupAllDabsStubs') && article.namespace) { return false; }		var sectStub = 0; var realStub = 0; if (pg.re.stub.test(data)) { var s = data.parenSplit(pg.re.stub); for (var i = 1; i < s.length; i = i + 2) { if (s[i]) { ++sectStub; } else { ++realStub; }			}		}		return { real: realStub, sect: sectStub }; }

function isValidImageName(str) { // extend as needed... return str.indexOf('{') == -1; }

function isInStrippableNamespace(article) { // Does the namespace allow subpages // Note, would be better if we had access to wgNamespacesWithSubpages return article.namespaceId !== 0; }

function isInMainNamespace(article) { return article.namespaceId === 0; }

function anchorContainsImage(a) { // iterate over children of anchor a		// see if any are images if (a === null) { return false; }		var kids = a.childNodes; for (var i = 0; i < kids.length; ++i) { if (kids[i].nodeName == 'IMG') { return true; }		}		return false; }	//</NOLITE> function isPopupLink(a) { // NB for performance reasons, TOC links generally return true // they should be stripped out later

if (!markNopopupSpanLinks.done) { markNopopupSpanLinks; }		if (a.inNopopupSpan) { return false; }

// FIXME is this faster inline? if (a.onmousedown || a.getAttribute('nopopup')) { return false; }		var h = a.href; if (h === document.location.href + '#') { return false; }		if (!pg.re.basenames.test(h)) { return false; }		if (!pg.re.urlNoPopup.test(h)) { return true; }		return (			(pg.re.email.test(h) || pg.re.contribs.test(h) || pg.re.backlinks.test(h) || pg.re.specialdiff.test(h)) &&			h.indexOf('&limit=') == -1		); }

function markNopopupSpanLinks { if (!getValueOf('popupOnlyArticleLinks')) fixVectorMenuPopups;

var s = $('.nopopups').toArray; for (var i = 0; i < s.length; ++i) { var as = s[i].getElementsByTagName('a'); for (var j = 0; j < as.length; ++j) { as[j].inNopopupSpan = true; }		}

markNopopupSpanLinks.done = true; }

function fixVectorMenuPopups { $('nav.vector-menu h3:first a:first').prop('inNopopupSpan', true); }	// ENDFILE: titles.js

// STARTFILE: getpage.js	////////////////////////////////////////////////// // Wiki-specific downloading //

// Schematic for a getWiki call //	//            getPageWithCaching //					|	//	  false		|		  true // getPage<-[findPictureInCache]->-onComplete(a fake download) //  \.	//	 (async)->addPageToCache(download)->-onComplete(download)

// check cache to see if page exists

function getPageWithCaching(url, onComplete, owner) { log('getPageWithCaching, url=' + url); var i = findInPageCache(url); var d;		if (i > -1) { d = fakeDownload(				url,				owner.idNumber,				onComplete,				pg.cache.pages[i].data,				pg.cache.pages[i].lastModified,				owner			); } else { d = getPage(url, onComplete, owner); if (d && owner && owner.addDownload) { owner.addDownload(d); d.owner = owner; }		}	}

function getPage(url, onComplete, owner) { log('getPage'); var callback = function (d) { if (!d.aborted) { addPageToCache(d); onComplete(d); }		};		return startDownload(url, owner.idNumber, callback); }

function findInPageCache(url) { for (var i = 0; i < pg.cache.pages.length; ++i) { if (url == pg.cache.pages[i].url) { return i;			} }		return -1; }

function addPageToCache(download) { log('addPageToCache ' + download.url); var page = { url: download.url, data: download.data, lastModified: download.lastModified, };		return pg.cache.pages.push(page); }	// ENDFILE: getpage.js

// STARTFILE: parensplit.js	////////////////////////////////////////////////// // parenSplit

// String.prototype.parenSplit should do what ECMAscript says String.prototype.split does, // interspersing paren matches (regex capturing groups) between the split elements. // i.e. 'abc'.split(/(b)/)) should return ['a','b','c'], not ['a','c']

if (String('abc'.split(/(b)/)) != 'a,b,c') { // broken String.split, e.g. konq, IE < 10 String.prototype.parenSplit = function (re) { re = nonGlobalRegex(re); var s = this; var m = re.exec(s); var ret = []; while (m && s) { // without the following loop, we have // 'ab'.parenSplit(/a|(b)/) != 'ab'.split(/a|(b)/) for (var i = 0; i < m.length; ++i) { if (typeof m[i] == 'undefined') m[i] = ''; }				ret.push(s.substring(0, m.index)); ret = ret.concat(m.slice(1)); s = s.substring(m.index + m[0].length); m = re.exec(s); }			ret.push(s); return ret; };	} else { String.prototype.parenSplit = function (re) { return this.split(re); };		String.prototype.parenSplit.isNative = true; }

function nonGlobalRegex(re) { var s = re.toString; var flags = ''; for (var j = s.length; s.charAt(j) != '/'; --j) { if (s.charAt(j) != 'g') { flags += s.charAt(j); }		}		var t = s.substring(1, j); return RegExp(t, flags); }	// ENDFILE: parensplit.js

// STARTFILE: tools.js	// IE madness with encoding // ========================	//	// suppose throughout that the page is in utf8, like wikipedia //	// if a is an anchor DOM element and a.href should consist of	// // http://host.name.here/wiki/foo?bar=baz //	// then IE gives foo as "latin1-encoded" utf8; we have foo = decode_utf8(decodeURI(foo_ie)) // but IE gives bar=baz correctly as plain utf8 //	// -	//	// IE's xmlhttp doesn't understand utf8 urls. Have to use encodeURI here. //	// -	//	// summat else

// Source: http://aktuell.de.selfhtml.org/artikel/javascript/utf8b64/utf8.htm

//<NOLITE>

function getJsObj(json) { try { var json_ret = JSON.parse(json); if (json_ret.warnings) { for (var w = 0; w < json_ret.warnings.length; w++) { if (json_ret.warnings[w]['*']) { log(json_ret.warnings[w]['*']); } else { log(json_ret.warnings[w].warnings); }				}			} else if (json_ret.error) { errlog(json_ret.error.code + ': ' + json_ret.error.info); }			return json_ret; } catch (someError) { errlog('Something went wrong with getJsObj, json=' + json); return 1; }	}

function anyChild(obj) { for (var p in obj) { return obj[p]; }		return null; }

//</NOLITE>

function upcaseFirst(str) { if (typeof str != typeof  || str === ) return ''; return str.charAt(0).toUpperCase + str.substring(1); }

function findInArray(arr, foo) { if (!arr || !arr.length) { return -1; }		var len = arr.length; for (var i = 0; i < len; ++i) { if (arr[i] == foo) { return i;			} }		return -1; }

/* eslint-disable no-unused-vars */ function nextOne(array, value) { // NB if the array has two consecutive entries equal //	then this will loop on successive calls var i = findInArray(array, value); if (i < 0) { return null; }		return array[i + 1]; }	/* eslint-enable no-unused-vars */

function literalizeRegex(str) { return mw.util.escapeRegExp(str); }

String.prototype.entify = function { //var shy='&shy;'; return this.split('&') .join('&amp;') .split('<') .join('&lt;') .split('>') .join('&gt;' /*+shy*/) .split('"')			.join('&quot;');	};

// Array filter function function removeNulls(val) { return val !== null; }

function joinPath(list) { return list.filter(removeNulls).join('/'); }

function simplePrintf(str, subs) { if (!str || !subs) { return str; }		var ret = []; var s = str.parenSplit(/(%s|\$[0-9]+)/); var i = 0; do { ret.push(s.shift); if (!s.length) { break; }			var cmd = s.shift; if (cmd == '%s') { if (i < subs.length) { ret.push(subs[i]); } else { ret.push(cmd); }				++i; } else { var j = parseInt(cmd.replace('$', ''), 10) - 1; if (j > -1 && j < subs.length) { ret.push(subs[j]); } else { ret.push(cmd); }			}		} while (s.length > 0); return ret.join(''); }

/* eslint-disable no-unused-vars */ function isString(x) { return typeof x === 'string' || x instanceof String; }

function isNumber(x) { return typeof x === 'number' || x instanceof Number; }

function isRegExp(x) { return x instanceof RegExp; }

function isArray(x) { return x instanceof Array; }

function isObject(x) { return x instanceof Object; }

function isFunction(x) { return !isRegExp(x) && (typeof x === 'function' || x instanceof Function); }	/* eslint-enable no-unused-vars */

function repeatString(s, mult) { var ret = ''; for (var i = 0; i < mult; ++i) { ret += s;		} return ret; }

function zeroFill(s, min) { min = min || 2; var t = s.toString; return repeatString('0', min - t.length) + t;	}

function map(f, o) { if (isArray(o)) { return map_array(f, o); }		return map_object(f, o); }	function map_array(f, o) { var ret = []; for (var i = 0; i < o.length; ++i) { ret.push(f(o[i])); }		return ret; }	function map_object(f, o) { var ret = {}; for (var i in o) { ret[o] = f(o[i]); }		return ret; }

pg.escapeQuotesHTML = function (text) { return text .replace(/&/g, '&amp;') .replace(/"/g, '&quot;')			.replace(/</g, '&lt;')			.replace(/>/g, '&gt;');	};

pg.unescapeQuotesHTML = function (html) { // From https://stackoverflow.com/a/7394787 // This seems to be implemented correctly on all major browsers now, so we // don't have to make our own function. var txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; };

// ENDFILE: tools.js

// STARTFILE: dab.js	//<NOLITE> //////////////////////////////////////////////////	// Dab-fixing code //

function retargetDab(newTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) { log('retargetDab: newTarget=' + newTarget + ' oldTarget=' + oldTarget); return changeLinkTargetLink({			newTarget: newTarget,			text: newTarget.split(' ').join(' '),			hint: tprintf('disambigHint', [newTarget]),			summary: simplePrintf(getValueOf('popupFixDabsSummary'), [ friendlyCurrentArticleName, newTarget, ]),			clickButton: getValueOf('popupDabsAutoClick'),			minor: true,			oldTarget: oldTarget,			watch: getValueOf('popupWatchDisambiggedPages'),			title: titleToEdit,		}); }

function listLinks(wikitext, oldTarget, titleToEdit) { // mediawiki strips trailing spaces, so we do the same // testcase: https://en.wikipedia.org/w/index.php?title=Radial&oldid=97365633 var reg = RegExp('\\[\\[([^|]*?) *(\\||\\]\\])', 'gi'); var ret = []; var splitted = wikitext.parenSplit(reg); // ^[a-z]+ should match interwiki links, hopefully (case-insensitive) // and ^[a-z]* should match those and Category... style links too var omitRegex = RegExp('^[a-z]*:|^[Ss]pecial:|^[Ii]mage|^[Cc]ategory'); var friendlyCurrentArticleName = oldTarget.toString; var wikPos = getValueOf('popupDabWiktionary');

for (var i = 1; i < splitted.length; i = i + 3) { if (				typeof splitted[i] == typeof 'string' &&				splitted[i].length > 0 &&				!omitRegex.test(splitted[i])			) { ret.push(retargetDab(splitted[i], oldTarget, friendlyCurrentArticleName, titleToEdit)); } /* if */ } /* for loop */

ret = rmDupesFromSortedList(ret.sort);

if (wikPos) { var wikTarget = 'wiktionary:' + friendlyCurrentArticleName.replace(RegExp('^(.+)\\s+[(][^)]+[)]\\s*$'), '$1');

var meth; if (wikPos.toLowerCase == 'first') { meth = 'unshift'; } else { meth = 'push'; }

ret[meth](retargetDab(wikTarget, oldTarget, friendlyCurrentArticleName, titleToEdit)); }

ret.push(			changeLinkTargetLink({ newTarget: null, text: popupString('remove this link').split(' ').join(' '), hint: popupString('remove all links to this disambig page from this article'), clickButton: getValueOf('popupDabsAutoClick'), oldTarget: oldTarget, summary: simplePrintf(getValueOf('popupRmDabLinkSummary'), [friendlyCurrentArticleName]), watch: getValueOf('popupWatchDisambiggedPages'), title: titleToEdit, })		);		return ret; }

function rmDupesFromSortedList(list) { var ret = []; for (var i = 0; i < list.length; ++i) { if (ret.length === 0 || list[i] != ret[ret.length - 1]) { ret.push(list[i]); }		}		return ret; }

function makeFixDab(data, navpop) { // grab title from parent popup if there is one; default exists in changeLinkTargetLink var titleToEdit = navpop.parentPopup && navpop.parentPopup.article.toString; var list = listLinks(data, navpop.originalArticle, titleToEdit); if (list.length === 0) { log('listLinks returned empty list'); return null; }		var html = ' ' + popupString('Click to disambiguate this link to:') + ' '; html += list.join(', '); return html; }

function makeFixDabs(wikiText, navpop) { if (			getValueOf('popupFixDabs') &&			isDisambig(wikiText, navpop.article) &&			Title.fromURL(location.href).namespaceId != pg.nsSpecialId &&			navpop.article.talkPage		) { setPopupHTML(makeFixDab(wikiText, navpop), 'popupFixDab', navpop.idNumber); }	}

function popupRedlinkHTML(article) { return changeLinkTargetLink({			newTarget: null,			text: popupString('remove this link').split(' ').join(' '),			hint: popupString('remove all links to this page from this article'),			clickButton: getValueOf('popupRedlinkAutoClick'),			oldTarget: article.toString,			summary: simplePrintf(getValueOf('popupRedlinkSummary'), [article.toString]),		}); }	//</NOLITE> // ENDFILE: dab.js

// STARTFILE: htmloutput.js

// this has to use a timer loop as we don't know if the DOM element exists when we want to set the text function setPopupHTML(str, elementId, popupId, onSuccess, append) { if (typeof popupId === 'undefined') { //console.error('popupId is not defined in setPopupHTML, html='+str.substring(0,100)); popupId = pg.idNumber; }

var popupElement = document.getElementById(elementId + popupId); if (popupElement) { if (!append) { popupElement.innerHTML = ''; }			if (isString(str)) { popupElement.innerHTML += str; } else { popupElement.appendChild(str); }			if (onSuccess) { onSuccess; }			setTimeout(checkPopupPosition, 100); return true; } else { // call this function again in a little while... setTimeout(function {				setPopupHTML(str, elementId, popupId, onSuccess);			}, 600); }		return null; }

//<NOLITE> function setPopupTrailer(str, id) { return setPopupHTML(str, 'popupData', id); }	//</NOLITE>

// args.navpopup is mandatory // optional: args.redir, args.redirTarget // FIXME: ye gods, this is ugly stuff function fillEmptySpans(args) { // if redir is present and true then redirTarget is mandatory var redir = true; var rcid; if (typeof args != 'object' || typeof args.redir == 'undefined' || !args.redir) { redir = false; }		var a = args.navpopup.parentAnchor;

var article, hint = null, oldid = null, params = {}; if (redir && typeof args.redirTarget == typeof {}) { article = args.redirTarget; //hint=article.hintValue; } else { article = new Title.fromAnchor(a); hint = a.originalTitle || article.hintValue; params = parseParams(a.href); oldid = getValueOf('popupHistoricalLinks') ? params.oldid : null; rcid = params.rcid; }		var x = { article: article, hint: hint, oldid: oldid, rcid: rcid, navpop: args.navpopup, params: params, };

var structure = pg.structures[getValueOf('popupStructure')]; if (typeof structure != 'object') { setPopupHTML(				'popupError',				'Unknown structure (this should never happen): ' + pg.option.popupStructure,				args.navpopup.idNumber			); return; }		var spans = flatten(pg.misc.layout); var numspans = spans.length; var redirs = pg.misc.redirSpans;

for (var i = 0; i < numspans; ++i) { var found = redirs && redirs.indexOf(spans[i]) !== -1; //log('redir='+redir+', found='+found+', spans[i]='+spans[i]); if ((found && !redir) || (!found && redir)) { //log('skipping this set of the loop'); continue; }			var structurefn = structure[spans[i]]; if (structurefn === undefined) { // nothing to do for this structure part continue; }			var setfn = setPopupHTML; if (				getValueOf('popupActiveNavlinks') &&				(spans[i].indexOf('popupTopLinks') === 0 || spans[i].indexOf('popupRedirTopLinks') === 0)			) { setfn = setPopupTipsAndHTML; }			switch (typeof structurefn) { case 'function': log(						'running ' +							spans[i] +							'({article:' + x.article + ', hint:' + x.hint + ', oldid: ' + x.oldid + '})'					);					setfn(structurefn(x), spans[i], args.navpopup.idNumber); break; case 'string': setfn(structurefn, spans[i], args.navpopup.idNumber); break; default: errlog('unknown thing with label ' + spans[i] + ' (span index was ' + i + ')'); break; }		}	}

// flatten an array function flatten(list, start) { var ret = []; if (typeof start == 'undefined') { start = 0; }		for (var i = start; i < list.length; ++i) { if (typeof list[i] == typeof []) { return ret.concat(flatten(list[i])).concat(flatten(list, i + 1)); } else { ret.push(list[i]); }		}		return ret; }

// Generate html for whole popup function popupHTML(a) { getValueOf('popupStructure'); var structure = pg.structures[pg.option.popupStructure]; if (typeof structure != 'object') { //return 'Unknown structure: '+pg.option.popupStructure; // override user choice pg.option.popupStructure = pg.optionDefault.popupStructure; return popupHTML(a); }		if (typeof structure.popupLayout != 'function') { return 'Bad layout'; }		pg.misc.layout = structure.popupLayout; if (typeof structure.popupRedirSpans === 'function') { pg.misc.redirSpans = structure.popupRedirSpans; } else { pg.misc.redirSpans = []; }		return makeEmptySpans(pg.misc.layout, a.navpopup); }

function makeEmptySpans(list, navpop) { var ret = ''; for (var i = 0; i < list.length; ++i) { if (typeof list[i] == typeof '') { ret += emptySpanHTML(list[i], navpop.idNumber, 'div'); } else if (typeof list[i] == typeof [] && list[i].length > 0) { ret = ret.parenSplit(RegExp('(</[^>]*?>$)')).join(makeEmptySpans(list[i], navpop)); } else if (typeof list[i] == typeof {} && list[i].nodeType) { ret += emptySpanHTML(list[i].name, navpop.idNumber, list[i].nodeType); }		}		return ret; }

function emptySpanHTML(name, id, tag, classname) { tag = tag || 'span'; if (!classname) { classname = emptySpanHTML.classAliases[name]; }		classname = classname || name; if (name == getValueOf('popupDragHandle')) { classname += ' popupDragHandle'; }		return simplePrintf('<%s id="%s" class="%s"></%s>', [tag, name + id, classname, tag]); }	emptySpanHTML.classAliases = { popupSecondPreview: 'popupPreview' };

// generate html for popup image // <a id="popupImageLinkn"><img id="popupImagen"> // where n=idNumber function imageHTML(article, idNumber) { return simplePrintf(			'<a id="popupImageLink$1">' +				'<img align="right" valign="top" id="popupImg$1" style="display: none;"> ' +				'</a>',			[idNumber]		); }

function popTipsSoonFn(id, when, popData) { if (!when) { when = 250; }		var popTips = function { setupTooltips(document.getElementById(id), false, true, popData); };		return function { setTimeout(popTips, when, popData); };	}

function setPopupTipsAndHTML(html, divname, idnumber, popData) { setPopupHTML(			html,			divname,			idnumber,			getValueOf('popupSubpopups') ? popTipsSoonFn(divname + idnumber, null, popData) : null		); }	// ENDFILE: htmloutput.js

// STARTFILE: mouseout.js	////////////////////////////////////////////////// // fuzzy checks

function fuzzyCursorOffMenus(x, y, fuzz, parent) { if (!parent) { return null; }		var uls = parent.getElementsByTagName('ul'); for (var i = 0; i < uls.length; ++i) { if (uls[i].className == 'popup_menu') { if (uls[i].offsetWidth > 0) return false; } // else {document.title+='.';} }		return true; }

function checkPopupPosition { // stop the popup running off the right of the screen // FIXME avoid pg.current.link if (pg.current.link && pg.current.link.navpopup) pg.current.link.navpopup.limitHorizontalPosition; }

function mouseOutWikiLink { //console ('mouseOutWikiLink'); var a = this;

removeModifierKeyHandler(a);

if (a.navpopup === null || typeof a.navpopup === 'undefined') return; if (!a.navpopup.isVisible) { a.navpopup.banish; return; }		restoreTitle(a); Navpopup.tracker.addHook(posCheckerHook(a.navpopup)); }

function posCheckerHook(navpop) { return function { if (!navpop.isVisible) { return true; /* remove this hook */ }			if (Navpopup.tracker.dirty) { return false; }			var x = Navpopup.tracker.x,				y = Navpopup.tracker.y;			var mouseOverNavpop = navpop.isWithin(x, y, navpop.fuzz, navpop.mainDiv) || !fuzzyCursorOffMenus(x, y, navpop.fuzz, navpop.mainDiv);

// FIXME it'd be prettier to do this internal to the Navpopup objects var t = getValueOf('popupHideDelay'); if (t) { t = t * 1000; }			if (!t) { if (!mouseOverNavpop) { if (navpop.parentAnchor) { restoreTitle(navpop.parentAnchor); }					navpop.banish; return true; /* remove this hook */ }				return false; }			// we have a hide delay set var d = +new Date; if (!navpop.mouseLeavingTime) { navpop.mouseLeavingTime = d;				return false; }			if (mouseOverNavpop) { navpop.mouseLeavingTime = null; return false; }			if (d - navpop.mouseLeavingTime > t) { navpop.mouseLeavingTime = null; navpop.banish; return true; /* remove this hook */ }			return false; };	}

function runStopPopupTimer(navpop) { // at this point, we should have left the link but remain within the popup // so we call this function again until we leave the popup. if (!navpop.stopPopupTimer) { navpop.stopPopupTimer = setInterval(posCheckerHook(navpop), 500); navpop.addHook(				function {					clearInterval(navpop.stopPopupTimer);				},				'hide',				'before'			); }	}	// ENDFILE: mouseout.js

// STARTFILE: previewmaker.js	/** * @fileoverview * Defines the {@link Previewmaker} object, which generates short previews from wiki markup. */

/**	 * Creates a new Previewmaker * @constructor * @class The Previewmaker class. Use an instance of this to generate short previews from Wikitext. * @param {String} wikiText The Wikitext source of the page we wish to preview. * @param {String} baseUrl The url we should prepend when creating relative urls. * @param {Navpopup} owner The navpop associated to this preview generator */	function Previewmaker(wikiText, baseUrl, owner) { /** The wikitext which is manipulated to generate the preview. */		this.originalData = wikiText; this.baseUrl = baseUrl; this.owner = owner;

this.maxCharacters = getValueOf('popupMaxPreviewCharacters'); this.maxSentences = getValueOf('popupMaxPreviewSentences');

this.setData; }

Previewmaker.prototype.setData = function { var maxSize = Math.max(10000, 2 * this.maxCharacters); this.data = this.originalData.substring(0, maxSize); };

/**	 * Remove HTML comments * @private */	Previewmaker.prototype.killComments = function { // this also kills one trailing newline, eg diamyo this.data = this.data.replace(			RegExp('^\\n|\\n(?=\\n)|', 'g'),			''		); };

/**	 * @private */	Previewmaker.prototype.killDivs = function { // say goodbye, divs (can be nested, so use * not *?) this.data = this.data.replace(RegExp('< *div[^>]* *>[\\s\\S]*?< */ *div *>', 'gi'), ''); };

/**	 * @private */	Previewmaker.prototype.killGalleries = function { this.data = this.data.replace(RegExp('< *gallery[^>]* *>[\\s\\S]*?< */ *gallery *>', 'gi'), ''); };

/**	 * @private */	Previewmaker.prototype.kill = function (opening, closing, subopening, subclosing, repl) { var oldk = this.data; var k = this.killStuff(this.data, opening, closing, subopening, subclosing, repl); while (k.length < oldk.length) { oldk = k;			k = this.killStuff(k, opening, closing, subopening, subclosing, repl); }		this.data = k;	};

/**	 * @private */	Previewmaker.prototype.killStuff = function (		txt,		opening,		closing,		subopening,		subclosing,		repl	) { var op = this.makeRegexp(opening); var cl = this.makeRegexp(closing, '^'); var sb = subopening ? this.makeRegexp(subopening, '^') : null; var sc = subclosing ? this.makeRegexp(subclosing, '^') : cl; if (!op || !cl) { alert('Navigation Popups error: op or cl is null! something is wrong.'); return; }		if (!op.test(txt)) { return txt; }		var ret = ''; var opResult = op.exec(txt); ret = txt.substring(0, opResult.index); txt = txt.substring(opResult.index + opResult[0].length); var depth = 1; while (txt.length > 0) { var removal = 0; if (depth == 1 && cl.test(txt)) { depth--; removal = cl.exec(txt)[0].length; } else if (depth > 1 && sc.test(txt)) { depth--; removal = sc.exec(txt)[0].length; } else if (sb && sb.test(txt)) { depth++; removal = sb.exec(txt)[0].length; }			if (!removal) { removal = 1; }			txt = txt.substring(removal); if (depth === 0) { break; }		}		return ret + (repl || '') + txt; };

/**	 * @private */	Previewmaker.prototype.makeRegexp = function (x, prefix, suffix) { prefix = prefix || ''; suffix = suffix || ''; var reStr = ''; var flags = ''; if (isString(x)) { reStr = prefix + literalizeRegex(x) + suffix; } else if (isRegExp(x)) { var s = x.toString.substring(1); var sp = s.split('/'); flags = sp[sp.length - 1]; sp[sp.length - 1] = ''; s = sp.join('/'); s = s.substring(0, s.length - 1); reStr = prefix + s + suffix; } else { log('makeRegexp failed'); }

log('makeRegexp: got reStr=' + reStr + ', flags=' + flags); return RegExp(reStr, flags); };

/**	 * @private */	Previewmaker.prototype.killBoxTemplates = function { // taxobox removal... in fact, there's a saudiprincebox_begin, so let's be more general // also, have float_begin, ... float_end this.kill(RegExp('[{][{][^{}\\s|]*?(float|box)[_ ](begin|start)', 'i'), /[}][}]\s*/, '{{');

// infoboxes etc // from User:Zyxw/popups.js: kill frames too this.kill(RegExp('[{][{][^{}\\s|]*?(infobox|elementbox|frame)[_ ]', 'i'), /[}][}]\s*/, '{{'); };

/**	 * @private */	Previewmaker.prototype.killTemplates = function { this.kill('{{', '}}', '{', '}', ' '); };

/**	 * @private */	Previewmaker.prototype.killTables = function { // tables are bad, too // this can be slow, but it's an inprovement over a browser hang // torture test: Comparison_of_Intel_Central_Processing_Units this.kill('{|', /[|]}\s*/, '{|'); this.kill(/ '); return html.join(''); }

function adjustDate(d, offset) { // offset is in minutes var o = offset * 60 * 1000; return new Date(+d + o); }

/**	 * This relies on the Date parser understanding en-US dates, * which is pretty safe assumption, but not perfect. */	function convertTimeZone(date, timeZone) { return new Date(date.toLocaleString('en-US', { timeZone: timeZone })); }

function formattedDateTime(date) { // fallback for IE11 and unknown timezones if (useTimeOffset) { return formattedDate(date) + ' ' + formattedTime(date); }

if (getMWDateFormat === 'ISO 8601') { var d2 = convertTimeZone(date, getTimeZone); return (				map(zeroFill, [d2.getFullYear, d2.getMonth + 1, d2.getDate]).join('-') +				'T' +				map(zeroFill, [d2.getHours, d2.getMinutes, d2.getSeconds]).join(':')			); }

var options = getValueOf('popupDateTimeFormatterOptions'); options['timeZone'] = getTimeZone; return date.toLocaleString(getLocales, options); }

function formattedDate(date) { // fallback for IE11 and unknown timezones if (useTimeOffset) { // we adjust the UTC time, so we print the adjusted UTC, but not really UTC values var d2 = adjustDate(date, getTimeOffset); return map(zeroFill, [d2.getUTCFullYear, d2.getUTCMonth + 1, d2.getUTCDate]).join('-'); }

if (getMWDateFormat === 'ISO 8601') { var d2 = convertTimeZone(date, getTimeZone); return map(zeroFill, [d2.getFullYear, d2.getMonth + 1, d2.getDate]).join('-'); }

var options = getValueOf('popupDateFormatterOptions'); options['timeZone'] = getTimeZone; return date.toLocaleDateString(getLocales, options); }

function formattedTime(date) { // fallback for IE11 and unknown timezones if (useTimeOffset) { // we adjust the UTC time, so we print the adjusted UTC, but not really UTC values var d2 = adjustDate(date, getTimeOffset); return map(zeroFill, [d2.getUTCHours, d2.getUTCMinutes, d2.getUTCSeconds]).join(':'); }

if (getMWDateFormat === 'ISO 8601') { var d2 = convertTimeZone(date, getTimeZone); return map(zeroFill, [d2.getHours, d2.getMinutes, d2.getSeconds]).join(':'); }

var options = getValueOf('popupTimeFormatterOptions'); options['timeZone'] = getTimeZone; return date.toLocaleTimeString(getLocales, options); }

// Get the proper groupnames for the technicalgroups function fetchUserGroupNames(userinfoResponse) { var queryObj = getJsObj(userinfoResponse).query; var user = anyChild(queryObj.users); var messages = []; if (user.groups) { user.groups.forEach(function (groupName) {				if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'named'].indexOf(groupName) === -1) {					messages.push('group-' + groupName + '-member');				}			}); }		if (queryObj.globaluserinfo && queryObj.globaluserinfo.groups) { queryObj.globaluserinfo.groups.forEach(function (groupName) {				messages.push('group-' + groupName + '-member');			}); }		return getMwApi.loadMessagesIfMissing(messages); }

function showAPIPreview(queryType, html, id, navpop, download) { // DJ: done var target = 'popupPreview'; completedNavpopTask(navpop);

switch (queryType) { case 'imagelinks': case 'category': target = 'popupPostPreview'; break; case 'userinfo': target = 'popupUserData'; break; case 'revision': insertPreview(download); return; }		setPopupTipsAndHTML(html, target, id); }

function APIrevisionPreviewHTML(article, download) { try { var jsObj = getJsObj(download.data); var page = anyChild(jsObj.query.pages); if (page.missing) { // TODO we need to fix this proper later on				download.owner = null; return; }			var content = page && page.revisions && page.revisions[0].contentmodel === 'wikitext' ? page.revisions[0].content : null; if (typeof content === 'string') { download.data = content; download.lastModified = new Date(page.revisions[0].timestamp); }		} catch (someError) { return 'Revision preview failed :(';		}	}

function APIbacklinksPreviewHTML(article, download /*, navpop*/) { try { var jsObj = getJsObj(download.data); var list = jsObj.query.backlinks;

var html = []; if (!list) { return popupString('No backlinks found'); }			for (var i = 0; i < list.length; i++) { var t = new Title(list[i].title); html.push(					'<a href="' + pg.wiki.titlebase + t.urlString + '">' + t.toString.entify + '</a>'				); }			html = html.join(', '); if (jsObj['continue'] && jsObj['continue'].blcontinue) { html += popupString(' and more'); }			return html; } catch (someError) { return 'backlinksPreviewHTML went wonky'; }	}

pg.fn.APIsharedImagePagePreviewHTML = function APIsharedImagePagePreviewHTML(obj) { log('APIsharedImagePagePreviewHTML'); var popupid = obj.requestid; if (obj.query && obj.query.pages) { var page = anyChild(obj.query.pages); var content = page && page.revisions && page.revisions[0].contentmodel === 'wikitext' ? page.revisions[0].content : null; if (				typeof content === 'string' &&				pg &&				pg.current &&				pg.current.link &&				pg.current.link.navpopup			) { /* Not entirely safe, but the best we can do */ var p = new Previewmaker(					content,					pg.current.link.navpopup.article,					pg.current.link.navpopup				); p.makePreview; setPopupHTML(p.html, 'popupSecondPreview', popupid); }		}	};

function APIimagepagePreviewHTML(article, download, navpop) { try { var jsObj = getJsObj(download.data); var page = anyChild(jsObj.query.pages); var content = page && page.revisions && page.revisions[0].contentmodel === 'wikitext' ? page.revisions[0].content : null; var ret = ''; var alt = ''; try { alt = navpop.parentAnchor.childNodes[0].alt; } catch (e) {} if (alt) { ret = ret + ' ' + popupString('Alt text:') + ' ' + pg.escapeQuotesHTML(alt); }			if (typeof content === 'string') { var p = prepPreviewmaker(content, article, navpop); p.makePreview; if (p.html) { ret += ' ' + p.html; }				if (getValueOf('popupSummaryData')) { var info = getPageInfo(content, download); log(info); setPopupTrailer(info, navpop.idNumber); }			}			if (page && page.imagerepository == 'shared') { var art = new Title(article); var encart = encodeURIComponent('File:' + art.stripNamespace); var shared_url = pg.wiki.apicommonsbase + '?format=json&formatversion=2' + '&callback=pg.fn.APIsharedImagePagePreviewHTML' + '&requestid=' + navpop.idNumber + '&action=query&prop=revisions&rvprop=content&titles=' + encart;

ret = ret + ' ' +					popupString('Image from Commons') + ': <a href="' +					pg.wiki.commonsbase +					'?title=' +					encart +					'">' + popupString('Description page') + '</a>'; mw.loader.load(shared_url); }			showAPIPreview(				'imagelinks',				APIimagelinksPreviewHTML(article, download),				navpop.idNumber,				download			); return ret; } catch (someError) { return 'API imagepage preview failed :(';		}	}

function APIimagelinksPreviewHTML(article, download) { try { var jsobj = getJsObj(download.data); var list = jsobj.query.imageusage; if (list) { var ret = []; for (var i = 0; i < list.length; i++) { ret.push(list[i].title); }				if (ret.length === 0) { return popupString('No image links found'); }				return ' ' + popupString('File links') + ' ' + linkList(ret); } else { return popupString('No image links found'); }		} catch (someError) { return 'Image links preview generation failed :(';		}	}

function APIcategoryPreviewHTML(article, download) { try { var jsobj = getJsObj(download.data); var list = jsobj.query.categorymembers; var ret = []; for (var p = 0; p < list.length; p++) { ret.push(list[p].title); }			if (ret.length === 0) { return popupString('Empty category'); }			ret = ' ' + tprintf('Category members (%s shown)', [ret.length]) + ' ' + linkList(ret); if (jsobj['continue'] && jsobj['continue'].cmcontinue) { ret += popupString(' and more'); }			return ret; } catch (someError) { return 'Category preview failed :(';		}	}

function APIuserInfoPreviewHTML(article, download) { var ret = []; var queryobj = {}; try { queryobj = getJsObj(download.data).query; } catch (someError) { return 'Userinfo preview failed :(';		}

var user = anyChild(queryobj.users); if (user) { var globaluserinfo = queryobj.globaluserinfo; if (user.invalid === '') { ret.push(popupString('Invalid user')); } else if (user.missing === '') { ret.push(popupString('Not a registered username')); }			if (user.blockedby) { if (user.blockpartial) { ret.push( + popupString('Has blocks') + ); } else { ret.push( + popupString('BLOCKED') + ); }			}			if (globaluserinfo && ('locked' in globaluserinfo || 'hidden' in globaluserinfo)) { var lockedSulAccountIsAttachedToThis = true; for (var i = 0; globaluserinfo.unattached && i < globaluserinfo.unattached.length; i++) { if (globaluserinfo.unattached[i].wiki === mw.config.get('wgDBname')) { lockedSulAccountIsAttachedToThis = false; break; }				}				if (lockedSulAccountIsAttachedToThis) { if ('locked' in globaluserinfo) ret.push( + popupString('LOCKED') + ); if ('hidden' in globaluserinfo) ret.push( + popupString('HIDDEN') + ); }			}			if (getValueOf('popupShowGender') && user.gender) { switch (user.gender) { case 'male': ret.push(popupString('he/him') + ' · '); break; case 'female': ret.push(popupString('she/her') + ' · '); break; }			}			if (user.groups) { user.groups.forEach(function (groupName) {					if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'named'].indexOf(groupName) === -1) {						ret.push( pg.escapeQuotesHTML(mw.message('group-' + groupName + '-member', user.gender).text) );					}				});			}			if (globaluserinfo && globaluserinfo.groups) { globaluserinfo.groups.forEach(function (groupName) {					ret.push( '<i>' + pg.escapeQuotesHTML(								mw.message('group-' + groupName + '-member', user.gender).text							) + '</i>' );				});			}			if (user.registration) ret.push(					pg.escapeQuotesHTML( (user.editcount ? user.editcount : '0') + popupString(' edits since: ') + (user.registration ? formattedDate(new Date(user.registration)) : '') )				);		}

if (queryobj.usercontribs && queryobj.usercontribs.length) { ret.push(				popupString('last edit on ') + formattedDate(new Date(queryobj.usercontribs[0].timestamp))			); }

if (queryobj.blocks) { ret.push(popupString('IP user')); //we only request list=blocks for IPs for (var l = 0; l < queryobj.blocks.length; l++) { var rbstr = queryobj.blocks[l].rangestart === queryobj.blocks[l].rangeend ? 'BLOCK' : 'RANGEBLOCK'; rbstr = !Array.isArray(queryobj.blocks[l].restrictions) ? 'Has ' + rbstr.toLowerCase + 's'					: rbstr + 'ED'; ret.push( + popupString(rbstr) + ); }		}

// if any element of ret ends with ' · ', merge it with the next element to avoid // the .join(', ') call inserting a comma after it		for (var m = 0; m < ret.length - 1; m++) { if (ret[m].length > 3 && ret[m].substring(ret[m].length - 3) === ' · ') { ret[m] = ret[m] + ret[m + 1]; ret.splice(m + 1, 1); // delete element at index m+1 m--; }		}

ret = ' ' + ret.join(', '); return ret; }

function APIcontribsPreviewHTML(article, download, navpop) { return APIhistoryPreviewHTML(article, download, navpop, true); }

function APIhistoryPreviewHTML(article, download, navpop, reallyContribs) { try { var jsobj = getJsObj(download.data); var edits = []; if (reallyContribs) { edits = jsobj.query.usercontribs; } else { edits = anyChild(jsobj.query.pages).revisions; }

var ret = editPreviewTable(article, edits, reallyContribs); return ret; } catch (someError) { return 'History preview failed :-(';		}	}

//</NOLITE> // ENDFILE: querypreview.js

// STARTFILE: debug.js	//////////////////////////////////////////////////////////////////// // Debugging functions ////////////////////////////////////////////////////////////////////

function setupDebugging { //<NOLITE> if (window.popupDebug) { // popupDebug is set from .version window.log = function (x) { //if(gMsg!='')gMsg += '\n'; gMsg+=time + ' ' + x; }; window.console.log(x); };			window.errlog = function (x) { window.console.error(x); };			log('Initializing logger'); } else { //</NOLITE> window.log = function {}; window.errlog = function {}; //<NOLITE> }		//</NOLITE> }	// ENDFILE: debug.js

// STARTFILE: images.js

// load image of type Title. function loadImage(image, navpop) { if (typeof image.stripNamespace != 'function') { alert('loadImages bad'); }		// API call to retrieve image info.

if (!getValueOf('popupImages')) return; if (!isValidImageName(image)) return false;

var art = image.urlString;

var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query'; url += '&prop=imageinfo&iiprop=url|mime&iiurlwidth=' + getValueOf('popupImageSizeLarge'); url += '&titles=' + art;

pendingNavpopTask(navpop); var callback = function (d) { popupsInsertImage(navpop.idNumber, navpop, d); };		var go = function { getPageWithCaching(url, callback, navpop); return true; };		if (navpop.visible || !getValueOf('popupLazyDownloads')) { go; } else { navpop.addHook(go, 'unhide', 'after', 'DOWNLOAD_IMAGE_QUERY_DATA'); }	}

function popupsInsertImage(id, navpop, download) { log('popupsInsertImage'); var imageinfo; try { var jsObj = getJsObj(download.data); var imagepage = anyChild(jsObj.query.pages); if (typeof imagepage.imageinfo === 'undefined') return; imageinfo = imagepage.imageinfo[0]; } catch (someError) { log('popupsInsertImage failed :(');			return;		}

var popupImage = document.getElementById('popupImg' + id); if (!popupImage) { log('could not find insertion point for image'); return; }

popupImage.width = getValueOf('popupImageSize'); popupImage.style.display = 'inline';

// Set the source for the image. if (imageinfo.thumburl) popupImage.src = imageinfo.thumburl; else if (imageinfo.mime.indexOf('image') === 0) { popupImage.src = imageinfo.url; log('a thumb could not be found, using original image'); } else log("fullsize imagethumb, but not sure if it's an image");

var a = document.getElementById('popupImageLink' + id); if (a === null) { return null; }

// Determine the action of the surrouding imagelink. switch (getValueOf('popupThumbAction')) { case 'imagepage': if (pg.current.article.namespaceId != pg.nsImageId) { a.href = imageinfo.descriptionurl; // FIXME: unreliable pg.idNumber popTipsSoonFn('popupImage' + id); break; }			/* falls through */ case 'sizetoggle': a.onclick = toggleSize; a.title = popupString('Toggle image size'); return; case 'linkfull': a.href = imageinfo.url; a.title = popupString('Open full-size image'); return; }	}

// Toggles the image between inline small and navpop fullwidth. // It's the same image, no actual sizechange occurs, only display width. function toggleSize { var imgContainer = this; if (!imgContainer) { alert('imgContainer is null :/'); return; }		var img = imgContainer.firstChild; if (!img) { alert('img is null :/'); return; }

if (!img.style.width || img.style.width === '') { img.style.width = '100%'; } else { img.style.width = ''; }	}

// Returns one title of an image from wikiText. function getValidImageFromWikiText(wikiText) { // nb in pg.re.image we're interested in the second bracketed expression // this may change if the regex changes :-(		//var match=pg.re.image.exec(wikiText);		var matched = null;		var match;		// strip html comments, used by evil bots :-( var t = removeMatchesUnless(			wikiText,			RegExp(''),			1,			RegExp('^<!--[^[]*popup', 'i')		);

while ((match = pg.re.image.exec(t))) { // now find a sane image name - exclude templates by seeking { var m = match[2] || match[6]; if (isValidImageName(m)) { matched = m;				break; }		}		pg.re.image.lastIndex = 0; if (!matched) { return null; }		return mw.config.get('wgFormattedNamespaces')[pg.nsImageId] + ':' + upcaseFirst(matched); }

function removeMatchesUnless(str, re1, parencount, re2) { var split = str.parenSplit(re1); var c = parencount + 1; for (var i = 0; i < split.length; ++i) { if (i % c === 0 || re2.test(split[i])) { continue; }			split[i] = ''; }		return split.join(''); }

//</NOLITE> // ENDFILE: images.js

// STARTFILE: namespaces.js	// Set up namespaces and other non-strings.js localization // (currently that means redirs too)

function setNamespaces { pg.nsSpecialId = -1; pg.nsMainspaceId = 0; pg.nsImageId = 6; pg.nsUserId = 2; pg.nsUsertalkId = 3; pg.nsCategoryId = 14; pg.nsTemplateId = 10; }

function setRedirs { var r = 'redirect'; var R = 'REDIRECT'; var redirLists = { //<NOLITE> ar: [R, 'تحويل'], be: [r, 'перанакіраваньне'], bg: [r, 'пренасочване', 'виж'], bs: [r, 'Preusmjeri', 'preusmjeri', 'PREUSMJERI'], bn: [R, 'পুনর্নির্দেশ'], cs: [R, 'PŘESMĚRUJ'], cy: [r, 'ail-cyfeirio'], de: [R, 'WEITERLEITUNG'], el: [R, 'ΑΝΑΚΑΤΕΥΘΥΝΣΗ'], eo: [R, 'ALIDIREKTU', 'ALIDIREKTI'], es: [R, 'REDIRECCIÓN'], et: [r, 'suuna'], ga: [r, 'athsheoladh'], gl: [r, 'REDIRECCIÓN', 'REDIRECIONAMENTO'], he: [R, 'הפניה'], hu: [R, 'ÁTIRÁNYÍTÁS'], is: [r, 'tilvísun', 'TILVÍSUN'], it: [R, 'RINVIA', 'Rinvia'], ja: [R, '転送'], mk: [r, 'пренасочување', 'види'], nds: [r, 'wiederleiden'], 'nds-nl': [R, 'DEURVERWIEZING', 'DUURVERWIEZING'], nl: [R, 'DOORVERWIJZING'], nn: [r, 'omdiriger'], pl: [R, 'PATRZ', 'PRZEKIERUJ', 'TAM'], pt: [R, 'redir'], ru: [R, 'ПЕРЕНАПРАВЛЕНИЕ', 'ПЕРЕНАПР'], sk: [r, 'presmeruj'], sr: [r, 'Преусмери', 'преусмери', 'ПРЕУСМЕРИ', 'Preusmeri', 'preusmeri', 'PREUSMERI'], tt: [R, 'yünältü', 'перенаправление', 'перенапр'], uk: [R, 'ПЕРЕНАПРАВЛЕННЯ', 'ПЕРЕНАПР'], vi: [r, 'đổi'], zh: [R, '重定向'], // no comma //</NOLITE> };		var redirList = redirLists[pg.wiki.lang] || [r, R]; // Mediawiki is very tolerant about what comes after the #redirect at the start pg.re.redirect = RegExp(			'^\\s*[#](' + redirList.join('|') + ').*?\\[{2}([^\\|\\]]*)(|[^\\]]*)?\\]{2}\\s*(.*)',			'i'		); }

function setInterwiki { if (pg.wiki.wikimedia) { // From https://meta.wikimedia.org/wiki/List_of_Wikipedias //en.wikipedia.org/w/api.php?action=sitematrix&format=json&smtype=language&smlangprop=code&formatversion=2 https: pg.wiki.interwiki = 'aa|ab|ace|af|ak|als|am|an|ang|ar|arc|arz|as|ast|av|ay|az|ba|bar|bat-smg|bcl|be|be-x-old|bg|bh|bi|bjn|bm|bn|bo|bpy|br|bs|bug|bxr|ca|cbk-zam|cdo|ce|ceb|ch|cho|chr|chy|ckb|co|cr|crh|cs|csb|cu|cv|cy|da|de|diq|dsb|dv|dz|ee|el|eml|en|eo|es|et|eu|ext|fa|ff|fi|fiu-vro|fj|fo|fr|frp|frr|fur|fy|ga|gag|gan|gd|gl|glk|gn|got|gu|gv|ha|hak|haw|he|hi|hif|ho|hr|hsb|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|ilo|io|is|it|iu|ja|jbo|jv|ka|kaa|kab|kbd|kg|ki|kj|kk|kl|km|kn|ko|koi|kr|krc|ks|ksh|ku|kv|kw|ky|la|lad|lb|lbe|lg|li|lij|lmo|ln|lo|lt|ltg|lv|map-bms|mdf|mg|mh|mhr|mi|mk|ml|mn|mo|mr|mrj|ms|mt|mus|mwl|my|myv|mzn|na|nah|nap|nds|nds-nl|ne|new|ng|nl|nn|no|nov|nrm|nv|ny|oc|om|or|os|pa|pag|pam|pap|pcd|pdc|pfl|pi|pih|pl|pms|pnb|pnt|ps|pt|qu|rm|rmy|rn|ro|roa-rup|roa-tara|ru|rue|rw|sa|sah|sc|scn|sco|sd|se|sg|sh|si|simple|sk|sl|sm|sn|so|sq|sr|srn|ss|st|stq|su|sv|sw|szl|ta|te|tet|tg|th|ti|tk|tl|tn|to|tpi|tr|ts|tt|tum|tw|ty|udm|ug|uk|ur|uz|ve|vec|vi|vls|vo|wa|war|wo|wuu|xal|xh|yi|yo|za|zea|zh|zh-classical|zh-min-nan|zh-yue|zu'; pg.re.interwiki = RegExp('^' + pg.wiki.interwiki + ':'); } else { pg.wiki.interwiki = null; pg.re.interwiki = RegExp('^$'); }	}

// return a regexp pattern matching all variants to write the given namespace function nsRe(namespaceId) { var imageNamespaceVariants = []; jQuery.each(mw.config.get('wgNamespaceIds'), function (_localizedNamespaceLc, _namespaceId) {			if (_namespaceId != namespaceId) return;			_localizedNamespaceLc = upcaseFirst(_localizedNamespaceLc);			imageNamespaceVariants.push( mw.util.escapeRegExp(_localizedNamespaceLc).split(' ').join('[ _]') );			imageNamespaceVariants.push(mw.util.escapeRegExp(encodeURI(_localizedNamespaceLc)));		});

return '(?:' + imageNamespaceVariants.join('|') + ')'; }

function nsReImage { return nsRe(pg.nsImageId); }	// ENDFILE: namespaces.js

// STARTFILE: selpop.js	//<NOLITE> function getEditboxSelection { // see http://www.webgurusforum.com/8/12/0 var editbox; try { editbox = document.editform.wpTextbox1; } catch (dang) { return; }		// IE, Opera if (document.selection) { return document.selection.createRange.text; }		// Mozilla var selStart = editbox.selectionStart; var selEnd = editbox.selectionEnd; return editbox.value.substring(selStart, selEnd); }

function doSelectionPopup { // popup if the selection looks like anything afterwards at all		// or [[foo|bartext without ']]' // or bar var sel = getEditboxSelection; var open = sel.indexOf();		var close = sel.indexOf(); if (open == -1 || (pipe == -1 && close == -1)) { return; }		if ((pipe != -1 && open > pipe) || (close != -1 && open > close)) { return; }		var article = new Title(sel.substring(open + 2, pipe < 0 ? close : pipe)); if (getValueOf('popupOnEditSelection') == 'boxpreview') { return doSeparateSelectionPopup(sel, article); }		if (close > 0 && sel.substring(close + 2).indexOf('[[') >= 0) {			return;		}		var a = document.createElement('a');		a.href = pg.wiki.titlebase + article.urlString;		mouseOverWikiLink2(a);		if (a.navpopup) {			a.navpopup.addHook(				function {					runStopPopupTimer(a.navpopup);				},				'unhide',				'after'			);		}	}

function doSeparateSelectionPopup(str, article) { var div = document.getElementById('selectionPreview'); if (!div) { div = document.createElement('div'); div.id = 'selectionPreview'; try { var box = document.editform.wpTextbox1; box.parentNode.insertBefore(div, box); } catch (error) { return; }		}		var p = prepPreviewmaker(str, article, newNavpopup(document.createElement('a'), article)); p.makePreview; if (p.html) { div.innerHTML = p.html; }		div.ranSetupTooltipsAlready = false; popTipsSoonFn('selectionPreview'); }	//</NOLITE> // ENDFILE: selpop.js

// STARTFILE: navpopup.js	/** * @fileoverview Defines two classes: {@link Navpopup} and {@link Mousetracker}. *	 *  describes popups: when they appear, where, what * they look like and so on. *	 *  "captures" the mouse using * .	 */

/**	 * Creates a new Mousetracker. * @constructor * @class The Mousetracker class. This monitors mouse movements and manages associated hooks. */	function Mousetracker { /**		 * Interval to regularly run the hooks anyway, in milliseconds. * @type Integer */		this.loopDelay = 400;

/**		 * Timer for the loop. * @type Timer */		this.timer = null;

/**		 * Flag - are we switched on? * @type Boolean */		this.active = false;

/**		 * Flag - are we probably inaccurate, i.e. not reflecting the actual mouse position? */		this.dirty = true;

/**		 * Array of hook functions. * @private * @type Array */		this.hooks = []; }

/**	 * Adds a hook, to be called when we get events. * @param {Function} f A function which is called as * . It should return   when it * wants to be removed, and  otherwise. */	Mousetracker.prototype.addHook = function (f) { this.hooks.push(f); };

/**	 * Runs hooks, passing them the x	 * and y coords of the mouse. Hook functions that return true are * passed to {@link Mousetracker#removeHooks} for removal. * @private */	Mousetracker.prototype.runHooks = function { if (!this.hooks || !this.hooks.length) { return; }		//log('Mousetracker.runHooks; we got some hooks to run'); var remove = false; var removeObj = {}; // this method gets called a LOT - // pre-cache some variables var x = this.x,			y = this.y,			len = this.hooks.length;

for (var i = 0; i < len; ++i) { //~ run the hook function, and remove it if it returns true if (this.hooks[i](x, y) === true) { remove = true; removeObj[i] = true; }		}		if (remove) { this.removeHooks(removeObj); }	};

/**	 * Removes hooks. * @private * @param {Object} removeObj An object whose keys are the index * numbers of functions for removal, with values that evaluate to true */	Mousetracker.prototype.removeHooks = function (removeObj) { var newHooks = []; var len = this.hooks.length; for (var i = 0; i < len; ++i) { if (!removeObj[i]) { newHooks.push(this.hooks[i]); }		}		this.hooks = newHooks; };

/**	 * Event handler for mouse wiggles. * We simply grab the event, set x and y and run the hooks. * This makes the cpu all hot and bothered :-(	 * @private	 * @param {Event} e Mousemove event	 */	Mousetracker.prototype.track = function (e) {		//~ Apparently this is needed in IE.		e = e || window.event;		var x, y;		if (e) {			if (e.pageX) {				x = e.pageX;				y = e.pageY;			} else if (typeof e.clientX != 'undefined') {				var left,					top,					docElt = document.documentElement;

if (docElt) { left = docElt.scrollLeft; }				left = left || document.body.scrollLeft || document.scrollLeft || 0;

if (docElt) { top = docElt.scrollTop; }				top = top || document.body.scrollTop || document.scrollTop || 0;

x = e.clientX + left; y = e.clientY + top; } else { return; }			this.setPosition(x, y); }	};

/**	 * Sets the x and y coordinates stored and takes appropriate action, * running hooks as appropriate. * @param {Integer} x, y Screen coordinates to set */	Mousetracker.prototype.setPosition = function (x, y) { this.x = x;		this.y = y;		if (this.dirty || this.hooks.length === 0) { this.dirty = false; return; }		if (typeof this.lastHook_x != 'number') { this.lastHook_x = -100; this.lastHook_y = -100; }		var diff = (this.lastHook_x - x) * (this.lastHook_y - y); diff = diff >= 0 ? diff : -diff; if (diff > 1) { this.lastHook_x = x;			this.lastHook_y = y;			if (this.dirty) { this.dirty = false; } else { this.runHooks; }		}	};

/**	 * Sets things in motion, unless they are already that is, registering an event handler on * . A half-hearted attempt is made to preserve the old event * handler if there is one. */	Mousetracker.prototype.enable = function { if (this.active) { return; }		this.active = true; //~ Save the current handler for mousemove events. This isn't too //~ robust, of course. this.savedHandler = document.onmousemove; //~ Gotta save @tt{this} again for the closure, and use apply for //~ the member function. var savedThis = this; document.onmousemove = function (e) { savedThis.track.apply(savedThis, [e]); };		if (this.loopDelay) { this.timer = setInterval(function {				//log('loop delay in mousetracker is working');				savedThis.runHooks;			}, this.loopDelay); }	};

/**	 * Disables the tracker, removing the event handler. */	Mousetracker.prototype.disable = function { if (!this.active) { return; }		if (typeof this.savedHandler === 'function') { document.onmousemove = this.savedHandler; } else { delete document.onmousemove; }		if (this.timer) { clearInterval(this.timer); }		this.active = false; };

/**	 * Creates a new Navpopup. * Gets a UID for the popup and * @param init Contructor object. If  is true or absent, the popup becomes draggable. * @constructor * @class The Navpopup class. This generates popup hints, and does some management of them. */	function Navpopup(/*init*/) { //alert('new Navpopup(init)');

/**		 * UID for each Navpopup instance. * Read-only. * @type integer */		this.uid = Navpopup.uid++;

/**		 * Read-only flag for current visibility of the popup. * @type boolean * @private */		this.visible = false;

/** Flag to be set when we want to cancel a previous request to * show the popup in a little while. * @private * @type boolean */		this.noshow = false;

/** Categorised list of hooks. * @see #runHooks * @see #addHook * @private * @type Object */		this.hooks = { create: [], unhide: [], hide: [], };

/**		 * list of unique IDs of hook functions, to avoid duplicates * @private */		this.hookIds = {};

/** List of downloads associated with the popup. * @private * @type Array */		this.downloads = [];

/**		 * Number of uncompleted downloads. * @type integer */		this.pending = null;

/**		 * Tolerance in pixels when detecting whether the mouse has left the popup. * @type integer */		this.fuzz = 5;

/**		 * Flag to toggle running {@link #limitHorizontalPosition} to regulate the popup's position. * @type boolean */		this.constrained = true;

/**		 * The popup width in pixels. * @private * @type integer */		this.width = 0;

/**		 * The popup width in pixels. * @private * @type integer */		this.height = 0;

/**		 * The main content DIV element. * @type HTMLDivElement */		this.mainDiv = null; this.createMainDiv;

//	if (!init || typeof init.popups_draggable=='undefined' || init.popups_draggable) { //		this.makeDraggable(true); //	}	}

/**	 * A UID for each Navpopup. This constructor property is just a counter. * @type integer * @private */	Navpopup.uid = 0;

/**	 * Retrieves the {@link #visible} attribute, indicating whether the popup is currently visible. * @type boolean */	Navpopup.prototype.isVisible = function { return this.visible; };

/**	 * Repositions popup using CSS style. * @private * @param {integer} x x-coordinate (px) * @param {integer} y y-coordinate (px) * @param {boolean} noLimitHor Don't call {@link #limitHorizontalPosition} */	Navpopup.prototype.reposition = function (x, y, noLimitHor) { log('reposition(' + x + ',' + y + ',' + noLimitHor + ')'); if (typeof x != 'undefined' && x !== null) { this.left = x;		} if (typeof y != 'undefined' && y !== null) { this.top = y;		} if (typeof this.left != 'undefined' && typeof this.top != 'undefined') { this.mainDiv.style.left = this.left + 'px'; this.mainDiv.style.top = this.top + 'px'; }		if (!noLimitHor) { this.limitHorizontalPosition; }		//console.log('navpop'+this.uid+' - (left,top)=(' + this.left + ',' + this.top + '), css=(' //+ this.mainDiv.style.left + ',' + this.mainDiv.style.top + ')'); };

/**	 * Prevents popups from being in silly locations. Hopefully. * Should not be run if {@link #constrained} is true. * @private */	Navpopup.prototype.limitHorizontalPosition = function { if (!this.constrained || this.tooWide) { return; }		this.updateDimensions; var x = this.left; var w = this.width; var cWidth = document.body.clientWidth;

//	log('limitHorizontalPosition: x='+x+		//			', this.left=' + this.left +		//			', this.width=' + this.width +		//			', cWidth=' + cWidth);

if (			x + w >= cWidth ||			(x > 0 && this.maxWidth && this.width < this.maxWidth && this.height > this.width && x > cWidth - this.maxWidth)		) { // This is a very nasty hack. There has to be a better way! // We find the "natural" width of the div by positioning it at the far left // then reset it so that it should be flush right (well, nearly) this.mainDiv.style.left = '-10000px'; this.mainDiv.style.width = this.maxWidth + 'px'; var naturalWidth = parseInt(this.mainDiv.offsetWidth, 10); var newLeft = cWidth - naturalWidth - 1; if (newLeft < 0) { newLeft = 0; this.tooWide = true; } // still unstable for really wide popups? log(				'limitHorizontalPosition: moving to (' + newLeft + ',' +					this.top + ');' +					' naturalWidth=' +					naturalWidth +					', clientWidth=' +					cWidth			); this.reposition(newLeft, null, true); }	};

/**	 * Counter indicating the z-order of the "highest" popup. * We start the z-index at 1000 so that popups are above everything * else on the screen. * @private * @type integer */	Navpopup.highest = 1000;

/**	 * Brings popup to the top of the z-order. * We increment the {@link #highest} property of the contructor here. * @private */	Navpopup.prototype.raise = function { this.mainDiv.style.zIndex = Navpopup.highest + 1; ++Navpopup.highest; };

/**	 * Shows the popup provided {@link #noshow} is not true. * Updates the position, brings the popup to the top of the z-order and unhides it. */	Navpopup.prototype.show = function { //document.title+='s'; if (this.noshow) { return; }		//document.title+='t'; this.reposition; this.raise; this.unhide; };

/**	 * Checks to see if the mouse pointer has * stabilised (checking every /2 milliseconds) and runs the * {@link #show} method if it has. * @param {integer} time The minimum time (ms) before the popup may be shown. */	Navpopup.prototype.showSoonIfStable = function (time) { log('showSoonIfStable, time=' + time); if (this.visible) { return; }		this.noshow = false;

//~ initialize these variables so that we never run @tt{show} after //~ just half the time this.stable_x = -10000; this.stable_y = -10000;

var stableShow = function { log('stableShow called'); var new_x = Navpopup.tracker.x,				new_y = Navpopup.tracker.y;			var dx = savedThis.stable_x - new_x, dy = savedThis.stable_y - new_y; var fuzz2 = 0; // savedThis.fuzz * savedThis.fuzz; //document.title += '[' + [savedThis.stable_x,new_x, savedThis.stable_y,new_y, dx, dy, fuzz2].join(',') + '] '; if (dx * dx <= fuzz2 && dy * dy <= fuzz2) { log('mouse is stable'); clearInterval(savedThis.showSoonStableTimer); savedThis.reposition.apply(savedThis, [new_x + 2, new_y + 2]); savedThis.show.apply(savedThis, []); savedThis.limitHorizontalPosition.apply(savedThis, []); return; }			savedThis.stable_x = new_x; savedThis.stable_y = new_y; };		var savedThis = this; this.showSoonStableTimer = setInterval(stableShow, time / 2); };

/**	 * Sets the {@link #noshow} flag and hides the popup. This should be called * when the mouse leaves the link before * (or after) it's actually been displayed. */	Navpopup.prototype.banish = function { log('banish called'); // hide and prevent showing with showSoon in the future this.noshow = true; if (this.showSoonStableTimer) { log('clearing showSoonStableTimer'); clearInterval(this.showSoonStableTimer); }		this.hide; };

/**	 * Runs hooks added with {@link #addHook}. * @private * @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide' * @param {String} when Controls exactly when the hook is run: either 'before' or 'after' */	Navpopup.prototype.runHooks = function (key, when) { if (!this.hooks[key]) { return; }		var keyHooks = this.hooks[key]; var len = keyHooks.length; for (var i = 0; i < len; ++i) { if (keyHooks[i] && keyHooks[i].when == when) { if (keyHooks[i].hook.apply(this, [])) { // remove the hook if (keyHooks[i].hookId) { delete this.hookIds[keyHooks[i].hookId]; }					keyHooks[i] = null; }			}		}	};

/**	 * Adds a hook to the popup. Hook functions are run with  set to refer to the * Navpopup instance, and no arguments. * @param {Function} hook The hook function. Functions that return true are deleted. * @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide' * @param {String} when Controls exactly when the hook is run: either 'before' or 'after' * @param {String} uid A truthy string identifying the hook function; if it matches another hook * in this position, it won't be added again. */	Navpopup.prototype.addHook = function (hook, key, when, uid) { when = when || 'after'; if (!this.hooks[key]) { return; }		// if uid is specified, don't add duplicates var hookId = null; if (uid) { hookId = [key, when, uid].join('|'); if (this.hookIds[hookId]) { return; }			this.hookIds[hookId] = true; }		this.hooks[key].push({ hook: hook, when: when, hookId: hookId }); };

/**	 * Creates the main DIV element, which contains all the actual popup content. * Runs hooks with key 'create'. * @private */	Navpopup.prototype.createMainDiv = function { if (this.mainDiv) { return; }		this.runHooks('create', 'before'); var mainDiv = document.createElement('div');

var savedThis = this; mainDiv.onclick = function (e) { savedThis.onclickHandler(e); };		mainDiv.className = this.className ? this.className : 'navpopup_maindiv'; mainDiv.id = mainDiv.className + this.uid;

mainDiv.style.position = 'absolute'; mainDiv.style.minWidth = '350px'; mainDiv.style.display = 'none'; mainDiv.className = 'navpopup';

// easy access to javascript object through DOM functions mainDiv.navpopup = this;

this.mainDiv = mainDiv; document.body.appendChild(mainDiv); this.runHooks('create', 'after'); };

/**	 * Calls the {@link #raise} method. * @private */	Navpopup.prototype.onclickHandler = function (/*e*/) { this.raise; };

/**	 * Makes the popup draggable, using a {@link Drag} object. * @private */	Navpopup.prototype.makeDraggable = function (handleName) { if (!this.mainDiv) { this.createMainDiv; }		var drag = new Drag; if (!handleName) { drag.startCondition = function (e) { try { if (!e.shiftKey) { return false; }				} catch (err) { return false; }				return true; };		}		var dragHandle; if (handleName) dragHandle = document.getElementById(handleName); if (!dragHandle) dragHandle = this.mainDiv; var np = this; drag.endHook = function (x, y) { Navpopup.tracker.dirty = true; np.reposition(x, y); };		drag.init(dragHandle, this.mainDiv); };

/**	 * Hides the popup using CSS. Runs hooks with key 'hide'. * Sets {@link #visible} appropriately. * {@link #banish} should be called externally instead of this method. * @private */	Navpopup.prototype.hide = function { this.runHooks('hide', 'before'); this.abortDownloads; if (typeof this.visible != 'undefined' && this.visible) { this.mainDiv.style.display = 'none'; this.visible = false; }		this.runHooks('hide', 'after'); };

/**	 * Shows the popup using CSS. Runs hooks with key 'unhide'. * Sets {@link #visible} appropriately. {@link #show} should be called externally instead of this method. * @private */	Navpopup.prototype.unhide = function { this.runHooks('unhide', 'before'); if (typeof this.visible != 'undefined' && !this.visible) { this.mainDiv.style.display = 'inline'; this.visible = true; }		this.runHooks('unhide', 'after'); };

/**	 * Sets the  attribute of the main div containing the popup content. * @param {String} html The HTML to set. */	Navpopup.prototype.setInnerHTML = function (html) { this.mainDiv.innerHTML = html; };

/**	 * Updates the {@link #width} and {@link #height} attributes with the CSS properties. * @private */	Navpopup.prototype.updateDimensions = function { this.width = parseInt(this.mainDiv.offsetWidth, 10); this.height = parseInt(this.mainDiv.offsetHeight, 10); };

/**	 * Checks if the point (x,y) is within {@link #fuzz} of the * {@link #mainDiv}. * @param {integer} x x-coordinate (px) * @param {integer} y y-coordinate (px) * @type boolean */	Navpopup.prototype.isWithin = function (x, y) { //~ If we're not even visible, no point should be considered as //~ being within the popup. if (!this.visible) { return false; }		this.updateDimensions; var fuzz = this.fuzz || 0; //~ Use a simple box metric here. return (			x + fuzz >= this.left &&			x - fuzz <= this.left + this.width &&			y + fuzz >= this.top &&			y - fuzz <= this.top + this.height		); };

/**	 * Adds a download to {@link #downloads}. * @param {Downloader} download */	Navpopup.prototype.addDownload = function (download) { if (!download) { return; }		this.downloads.push(download); };

/**	 * Aborts the downloads listed in {@link #downloads}. * @see Downloader#abort */	Navpopup.prototype.abortDownloads = function { for (var i = 0; i < this.downloads.length; ++i) { var d = this.downloads[i]; if (d && d.abort) { d.abort; }		}		this.downloads = []; };

/**	 * A {@link Mousetracker} instance which is a property of the constructor (pseudo-global). */	Navpopup.tracker = new Mousetracker; // ENDFILE: navpopup.js

// STARTFILE: diff.js	//<NOLITE> /*	 * Javascript Diff Algorithm * By John Resig (http://ejohn.org/) and en:User:Lupin *	 * More Info: * http://ejohn.org/projects/javascript-diff-algorithm/ */

function delFmt(x) { if (!x.length) { return ''; }		return "<del class='popupDiff'>" + x.join('') + ' '; }

function insFmt(x) { if (!x.length) { return ''; }		return "<ins class='popupDiff'>" + x.join('') + ' '; }

function countCrossings(a, b, i, eject) { // count the crossings on the edge starting at b[i] if (!b[i].row && b[i].row !== 0) { return -1; }		var count = 0; for (var j = 0; j < a.length; ++j) { if (!a[j].row && a[j].row !== 0) { continue; }			if ((j - b[i].row) * (i - a[j].row) > 0) { if (eject) { return true; }				count++; }		}		return count; }

function shortenDiffString(str, context) { var re = RegExp('(<del[\\s\\S]*? |<ins[\\s\\S]*? )');		var splitted = str.parenSplit(re); var ret = ['']; for (var i = 0; i < splitted.length; i += 2) { if (splitted[i].length < 2 * context) { ret[ret.length - 1] += splitted[i]; if (i + 1 < splitted.length) { ret[ret.length - 1] += splitted[i + 1]; }				continue; } else { if (i > 0) { ret[ret.length - 1] += splitted[i].substring(0, context); }				if (i + 1 < splitted.length) { ret.push(splitted[i].substring(splitted[i].length - context) + splitted[i + 1]); }			}		}		while (ret.length > 0 && !ret[0]) { ret = ret.slice(1); }		return ret; }

function diffString(o, n, simpleSplit) { var splitRe = RegExp('([\\{2}|[{]{2,3}|[}]{2,3}|[|]|=|<|>|[*:]+|\\s|\\b)');

// We need to split the strings o and n first, and entify the parts // individually, so that the HTML entities are never cut apart. (AxelBoldt) var out, i, oSplitted, nSplitted; if (simpleSplit) { oSplitted = o.split(/\b/); nSplitted = n.split(/\b/); } else { oSplitted = o.parenSplit(splitRe); nSplitted = n.parenSplit(splitRe); }		for (i = 0; i < oSplitted.length; ++i) { oSplitted[i] = oSplitted[i].entify; }		for (i = 0; i < nSplitted.length; ++i) { nSplitted[i] = nSplitted[i].entify; }

out = diff(oSplitted, nSplitted); var str = ''; var acc = []; // accumulator for prettier output

// crossing pairings -- eg 'A B' vs 'B A' -- cause problems, so let's iron them out // this doesn't always do things optimally but it should be fast enough var maxOutputPair = 0; for (i = 0; i < out.n.length; ++i) { if (out.n[i].paired) { if (maxOutputPair > out.n[i].row) { // tangle - delete pairing out.o[out.n[i].row] = out.o[out.n[i].row].text; out.n[i] = out.n[i].text; }				if (maxOutputPair < out.n[i].row) { maxOutputPair = out.n[i].row; }			}		}

// output the stuff preceding the first paired old line for (i = 0; i < out.o.length && !out.o[i].paired; ++i) { acc.push(out.o[i]); }		str += delFmt(acc); acc = [];

// main loop for (i = 0; i < out.n.length; ++i) { // output unpaired new "lines" while (i < out.n.length && !out.n[i].paired) { acc.push(out.n[i++]); }			str += insFmt(acc); acc = []; if (i < out.n.length) { // this new "line" is paired with the (out.n[i].row)th old "line" str += out.n[i].text; // output unpaired old rows starting after this new line's partner var m = out.n[i].row + 1; while (m < out.o.length && !out.o[m].paired) { acc.push(out.o[m++]); }				str += delFmt(acc); acc = []; }		}		return str; }

// see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object // FIXME: use obj.hasOwnProperty instead of this kludge! var jsReservedProperties = RegExp(		'^(constructor|prototype|__((define|lookup)[GS]etter)__' + '|eval|hasOwnProperty|propertyIsEnumerable' + '|to(Source|String|LocaleString)|(un)?watch|valueOf)$'	);

function diffBugAlert(word) { if (!diffBugAlert.list[word]) { diffBugAlert.list[word] = 1; alert('Bad word: ' + word + '\n\nPlease report this bug.'); }	}

diffBugAlert.list = {};

function makeDiffHashtable(src) { var ret = {}; for (var i = 0; i < src.length; i++) { if (jsReservedProperties.test(src[i])) { src[i] += ''; }			if (!ret[src[i]]) { ret[src[i]] = []; }			try { ret[src[i]].push(i); } catch (err) { diffBugAlert(src[i]); }		}		return ret; }

function diff(o, n) { // pass 1: make hashtable ns with new rows as keys var ns = makeDiffHashtable(n);

// pass 2: make hashtable os with old rows as keys var os = makeDiffHashtable(o);

// pass 3: pair unique new rows and matching unique old rows var i;		for (i in ns) { if (ns[i].length == 1 && os[i] && os[i].length == 1) { n[ns[i][0]] = { text: n[ns[i][0]], row: os[i][0], paired: true }; o[os[i][0]] = { text: o[os[i][0]], row: ns[i][0], paired: true }; }		}

// pass 4: pair matching rows immediately following paired rows (not necessarily unique) for (i = 0; i < n.length - 1; i++) { if (				n[i].paired &&				!n[i + 1].paired &&				n[i].row + 1 < o.length &&				!o[n[i].row + 1].paired &&				n[i + 1] == o[n[i].row + 1]			) { n[i + 1] = { text: n[i + 1], row: n[i].row + 1, paired: true }; o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1, paired: true }; }		}

// pass 5: pair matching rows immediately preceding paired rows (not necessarily unique) for (i = n.length - 1; i > 0; i--) { if (				n[i].paired &&				!n[i - 1].paired &&				n[i].row > 0 &&				!o[n[i].row - 1].paired &&				n[i - 1] == o[n[i].row - 1]			) { n[i - 1] = { text: n[i - 1], row: n[i].row - 1, paired: true }; o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1, paired: true }; }		}

return { o: o, n: n }; }	//</NOLITE> // ENDFILE: diff.js

// STARTFILE: init.js	function setSiteInfo { if (window.popupLocalDebug) { pg.wiki.hostname = 'en.wikipedia.org'; } else { pg.wiki.hostname = location.hostname; // use in preference to location.hostname for flexibility (?) }		pg.wiki.wikimedia = RegExp(			'(wiki([pm]edia|source|books|news|quote|versity|species|voyage|data)|metawiki|wiktionary|mediawiki)[.]org'		).test(pg.wiki.hostname); pg.wiki.wikia = RegExp('[.]wikia[.]com$', 'i').test(pg.wiki.hostname); pg.wiki.isLocal = RegExp('^localhost').test(pg.wiki.hostname); pg.wiki.commons = pg.wiki.wikimedia && pg.wiki.hostname != 'commons.wikimedia.org' ? 'commons.wikimedia.org' : null; pg.wiki.lang = mw.config.get('wgContentLanguage'); var port = location.port ? ':' + location.port : ''; pg.wiki.sitebase = pg.wiki.hostname + port; }

function setUserInfo { var params = { action: 'query', list: 'users', ususers: mw.config.get('wgUserName'), usprop: 'rights', };

pg.user.canReview = false; if (getValueOf('popupReview')) { getMwApi .get(params) .done(function (data) {					var rights = data.query.users[0].rights;					pg.user.canReview = rights.indexOf('review') !== -1; // TODO: Should it be a getValueOf('ReviewRight') ?				}); }	}

function fetchSpecialPageNames { var params = { action: 'query', meta: 'siteinfo', siprop: 'specialpagealiases', formatversion: 2, // cache for an hour uselang: 'content', maxage: 3600, };		return getMwApi .get(params) .then(function (data) {				pg.wiki.specialpagealiases = data.query.specialpagealiases;			}); }

function setTitleBase { var protocol = window.popupLocalDebug ? 'http:' : location.protocol; pg.wiki.articlePath = mw.config.get('wgArticlePath').replace(/\/\$1/, ''); // as in http://some.thing.com/wiki/Article pg.wiki.botInterfacePath = mw.config.get('wgScript'); pg.wiki.APIPath = mw.config.get('wgScriptPath') + '/api.php'; // default mediawiki setting is paths like http://some.thing.com/articlePath/index.php?title=foo

var titletail = pg.wiki.botInterfacePath + '?title='; //var titletail2 = joinPath([pg.wiki.botInterfacePath, 'wiki.phtml?title=']);

// other sites may need to add code here to set titletail depending on how their urls work

pg.wiki.titlebase = protocol + '//' + pg.wiki.sitebase + titletail; //pg.wiki.titlebase2 = protocol + '//' + joinPath([pg.wiki.sitebase, titletail2]); pg.wiki.wikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.botInterfacePath; pg.wiki.apiwikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.APIPath; pg.wiki.articlebase = protocol + '//' + pg.wiki.sitebase + pg.wiki.articlePath; pg.wiki.commonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.botInterfacePath; pg.wiki.apicommonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.APIPath; pg.re.basenames = RegExp(			'^(' + map(literalizeRegex, [					pg.wiki.titlebase, //pg.wiki.titlebase2,					pg.wiki.articlebase,				]).join('|') + ')'		);	}

//////////////////////////////////////////////////	// Global regexps

function setMainRegex { var reStart = '[^:]*://'; var preTitles = literalizeRegex(mw.config.get('wgScriptPath')) + '/(?:index[.]php|wiki[.]phtml)[?]title='; preTitles += '|' + literalizeRegex(pg.wiki.articlePath + '/');

var reEnd = '(' + preTitles + ')([^&?#]*)[^#]*(?:#(.+))?'; pg.re.main = RegExp(reStart + literalizeRegex(pg.wiki.sitebase) + reEnd); }

function buildSpecialPageGroup(specialPageObj) { var variants = []; variants.push(mw.util.escapeRegExp(specialPageObj['realname'])); variants.push(mw.util.escapeRegExp(encodeURI(specialPageObj['realname']))); specialPageObj.aliases.forEach(function (alias) {			variants.push(mw.util.escapeRegExp(alias));			variants.push(mw.util.escapeRegExp(encodeURI(alias)));		}); return variants.join('|'); }

function setRegexps { setMainRegex; var sp = nsRe(pg.nsSpecialId); pg.re.urlNoPopup = RegExp('((title=|/)' + sp + '(?:%3A|:)|section=[0-9]|^#$)');

pg.wiki.specialpagealiases.forEach(function (specialpage) {			if (specialpage.realname === 'Contributions') {				pg.re.contribs = RegExp( '(title=|/)' + sp + '(?:%3A|:)(?:' +						buildSpecialPageGroup(specialpage) +						')' + '(&target=|/|/' +						nsRe(pg.nsUserId) +						':)(.*)', 'i'				);			} else if (specialpage.realname === 'Diff') {				pg.re.specialdiff = RegExp( '/' + sp + '(?:%3A|:)(?:' + buildSpecialPageGroup(specialpage) + ')' + '/([^?#]*)', 'i'				);			} else if (specialpage.realname === 'Emailuser') {				pg.re.email = RegExp( '(title=|/)' + sp + '(?:%3A|:)(?:' +						buildSpecialPageGroup(specialpage) +						')' + '(&target=|/|/(?:' + nsRe(pg.nsUserId) + ':)?)(.*)',					'i'				);			} else if (specialpage.realname === 'Whatlinkshere') {				pg.re.backlinks = RegExp( '(title=|/)' + sp + '(?:%3A|:)(?:' +						buildSpecialPageGroup(specialpage) +						')' + '(&target=|/)([^&]*)', 'i'				);			}		});

//<NOLITE> var im = nsReImage; // note: tries to get images in infobox templates too, e.g. movie pages, album pages etc //					 (^|\) *		//					  (^|\)([^0-9\]]*([0-9]+) *px)? //														$4 = 120 as in 120px pg.re.image = RegExp(			'(^|\\[\\[)' +				im +				': *([^|\\]]*[^|\\] ])' +				'([^0-9\\]]*([0-9]+) *px)?|(?:\\n *[|]?|[|]) *' +				'(' + getValueOf('popupImageVarsRegexp') + ')' +				' *= *(?:\\[\\[ *)?(?:' +				im + ':)?' +				'([^|]*?)(?:\\]\\])? *[|]? *\\n',			'img'		); pg.re.imageBracketCount = 6;

pg.re.category = RegExp('\\[\\[' + nsRe(pg.nsCategoryId) + ': *([^|\\]]*[^|\\] ]) *', 'i'); pg.re.categoryBracketCount = 1;

pg.re.ipUser = RegExp(			'^' +				// IPv6				'(?::(?::|(?::[0-9A-Fa-f]{1,4}){1,7})|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}::|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){7})' +				// IPv4				'|(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' + '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]))$'		);

pg.re.stub = RegExp(getValueOf('popupStubRegexp'), 'im'); pg.re.disambig = RegExp(getValueOf('popupDabRegexp'), 'im');

//</NOLITE> // FIXME replace with general parameter parsing function, this is daft pg.re.oldid = RegExp('[?&]oldid=([^&]*)'); pg.re.diff = RegExp('[?&]diff=([^&]*)'); }

//////////////////////////////////////////////////	// miscellany

function setupCache { // page caching pg.cache.pages = []; }

function setMisc { pg.current.link = null; pg.current.links = []; pg.current.linksHash = {};

setupCache;

pg.timer.checkPopupPosition = null; pg.counter.loop = 0;

// ids change with each popup: popupImage0, popupImage1 etc pg.idNumber = 0;

// for myDecodeURI pg.misc.decodeExtras = [ { from: '%2C', to: ',' }, { from: '_', to: ' ' }, { from: '%24', to: '$' }, { from: '%26', to: '&' }, // no , ];	}

function getMwApi { if (!pg.api.client) { pg.api.userAgent = 'Navigation popups/1.0 (' + mw.config.get('wgServerName') + ')'; pg.api.client = new mw.Api({				ajax: {					headers: {						'Api-User-Agent': pg.api.userAgent,					},				},			}); }		return pg.api.client; }

// We need a callback since this might end up asynchronous because of // the mw.loader.using call. function setupPopups(callback) { if (setupPopups.completed) { if (typeof callback === 'function') { callback; }			return; }		// These dependencies should alse be enforced from the gadget, // but not everyone loads this as a gadget, so double check mw.loader .using([				'mediawiki.util',				'mediawiki.api',				'mediawiki.user',				'user.options',				'mediawiki.jqueryMsg',			]) .then(fetchSpecialPageNames) .then(function {				// NB translatable strings should be set up first (strings.js)				// basics				setupDebugging;				setSiteInfo;				setTitleBase;				setOptions; // see options.js				setUserInfo;

// namespaces etc setNamespaces; setInterwiki;

// regexps setRegexps; setRedirs;

// other stuff setMisc; setupLivePreview;

// main deal here setupTooltips; log('In setupPopups, just called setupTooltips'); Navpopup.tracker.enable;

setupPopups.completed = true; if (typeof callback === 'function') { callback; }			});	}	// ENDFILE: init.js

// STARTFILE: navlinks.js	//<NOLITE> //////////////////////////////////////////////////	// navlinks... let the fun begin //

function defaultNavlinkSpec { var str = ''; str += '<<mainlink|shortcut= >>'; if (getValueOf('popupLastEditLink')) { str += '*<<lastEdit|shortcut=/>>|<<lastContrib>>|<<sinceMe>>if(oldid){|<<oldEdit>>|<<diffCur>>}'; }

// user links // contribs - log - count - email - block // count only if applicable; block only if popupAdminLinks str += 'if(user){ <<contribs|shortcut=c>>*<<userlog|shortcut=L|log>>'; str += 'if(ipuser){*< >}if(wikimedia){*<<count|shortcut=#>>}'; str += 'if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>|<<blocklog|log>>}}';

// editing links // talkpage  -> edit|new - history - un|watch - article|edit // other page -> edit - history - un|watch - talk|edit|new var editstr = '<<edit|shortcut=e>>'; var editOldidStr = 'if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' + editstr + '}';		var historystr = '<<history|shortcut=h>>|<<editors|shortcut=E|>>'; var watchstr = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';

str += ' if(talk){' + editOldidStr + '|<<new|shortcut=+>>' + '*' +			historystr + '*' +			watchstr + '*' +			'<<article|shortcut=a>>|<<editArticle|edit>>' + '}else{' + // not a talk page editOldidStr + '*' +			historystr + '*' +			watchstr + '*' +			'<<talk|shortcut=t>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';

// misc links str += ' <<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>*<<move|shortcut=m>>';

// admin links str += 'if(admin){ <<unprotect|unprotectShort>>|<<protect|shortcut=p>>|<<protectlog|log>>*' + '<<undelete|undeleteShort>>|<<delete|shortcut=d>>|<<deletelog|log>>}'; return str; }

function navLinksHTML(article, hint, params) { //oldid, rcid) {		var str = ' ' + defaultNavlinkSpec + ' ';		// BAM		return navlinkStringToHTML(str, article, params);	}

function expandConditionalNavlinkString(s, article, z, recursionCount) { var oldid = z.oldid, rcid = z.rcid, diff = z.diff; // nested conditionals (up to 10 deep) are ok, hopefully! (work from the inside out) if (typeof recursionCount != typeof 0) { recursionCount = 0; }		var conditionalSplitRegex = RegExp(			//(1	 if	\\(	(2	2)	\\)	 {(3	3)}  (4   else	  {(5	 5)}  4)1)			'(?\\s*if\\s*\\(\\s*([\\w]*)\\s*\\)\\s*\\{([^{}]*)\\}(\\s*else\\s*\\{([^{}]*?)\\}|))',			'i'		); var splitted = s.parenSplit(conditionalSplitRegex); // $1: whole conditional // $2: test condition // $3: true expansion // $4: else clause (possibly empty) // $5: false expansion (possibly null) var numParens = 5; var ret = splitted[0]; for (var i = 1; i < splitted.length; i = i + numParens + 1) { var testString = splitted[i + 2 - 1]; var trueString = splitted[i + 3 - 1]; var falseString = splitted[i + 5 - 1]; if (typeof falseString == 'undefined' || !falseString) { falseString = ''; }			var testResult = null;

switch (testString) { case 'user': testResult = article.userName ? true : false; break; case 'talk': testResult = article.talkPage ? false : true; // talkPage converts _articles_ to talkPages break; case 'admin': testResult = getValueOf('popupAdminLinks') ? true : false; break; case 'oldid': testResult = typeof oldid != 'undefined' && oldid ? true : false; break; case 'rcid': testResult = typeof rcid != 'undefined' && rcid ? true : false; break; case 'ipuser': testResult = article.isIpUser ? true : false; break; case 'mainspace_en': testResult = isInMainNamespace(article) && pg.wiki.hostname == 'en.wikipedia.org'; break; case 'wikimedia': testResult = pg.wiki.wikimedia ? true : false; break; case 'diff': testResult = typeof diff != 'undefined' && diff ? true : false; break; }

switch (testResult) { case null: ret += splitted[i]; break; case true: ret += trueString; break; case false: ret += falseString; break; }

// append non-conditional string ret += splitted[i + numParens]; }		if (conditionalSplitRegex.test(ret) && recursionCount < 10) { return expandConditionalNavlinkString(ret, article, z, recursionCount + 1); }		return ret; }

function navlinkStringToArray(s, article, params) { s = expandConditionalNavlinkString(s, article, params); var splitted = s.parenSplit(RegExp('<<(.*?)>>')); var ret = []; for (var i = 0; i < splitted.length; ++i) { if (i % 2) { // i odd, so s is a tag var t = new navlinkTag; var ss = splitted[i].split('|'); t.id = ss[0]; for (var j = 1; j < ss.length; ++j) { var sss = ss[j].split('='); if (sss.length > 1) { t[sss[0]] = sss[1]; } else { // no assignment (no "="), so treat this as a title (overwriting the last one) t.text = popupString(sss[0]); }				}				t.article = article; var oldid = params.oldid, rcid = params.rcid, diff = params.diff; if (typeof oldid !== 'undefined' && oldid !== null) { t.oldid = oldid; }				if (typeof rcid !== 'undefined' && rcid !== null) { t.rcid = rcid; }				if (typeof diff !== 'undefined' && diff !== null) { t.diff = diff; }				if (!t.text && t.id !== 'mainlink') { t.text = popupString(t.id); }				ret.push(t); } else { // plain HTML ret.push(splitted[i]); }		}		return ret; }

function navlinkSubstituteHTML(s) { return s			.split('*') .join(getValueOf('popupNavLinkSeparator')) .split(' ') .join('<li class="popup_menu_row">') .split(' ') .join('</li>') .split(' ') .join('<ul class="popup_menu">') .split(' ') .join('</ul>'); }

function navlinkDepth(magic, s) { return s.split('<' + magic + '>').length - s.split('</' + magic + '>').length; }

// navlinkString: * becomes the separator //				<<foo|bar=baz|fubar>> becomes a foo-link with attribute bar='baz' //									 and visible text 'fubar' //				if(test){...} and if(test){...}else{...} work too (nested ok)

function navlinkStringToHTML(s, article, params) { //limitAlert(navlinkStringToHTML, 5, 'navlinkStringToHTML\n' + article + '\n' + (typeof article)); var p = navlinkStringToArray(s, article, params); var html = ''; var menudepth = 0; // nested menus not currently allowed, but doesn't do any harm to code for it		var menurowdepth = 0; for (var i = 0; i < p.length; ++i) { if (typeof p[i] == typeof '') { html += navlinkSubstituteHTML(p[i]); menudepth += navlinkDepth('menu', p[i]); menurowdepth += navlinkDepth('menurow', p[i]); //			if (menudepth === 0) { //				tagType='span'; //			} else if (menurowdepth === 0) { //				tagType='li'; //			} else { //				tagType = null; //			}			} else if (typeof p[i].type != 'undefined' && p[i].type == 'navlinkTag') { if (menudepth > 0 && menurowdepth === 0) { html += '<li class="popup_menu_item">' + p[i].html + '</li>'; } else { html += p[i].html; }			}		}		return html; }

function navlinkTag { this.type = 'navlinkTag'; }

navlinkTag.prototype.html = function { this.getNewWin; this.getPrintFunction; var html = ''; var opening, closing; var tagType = 'span'; if (!tagType) { opening = ''; closing = ''; } else { opening = '<' + tagType + ' class="popup_' + this.id + '">'; closing = '</' + tagType + '>'; }		if (typeof this.print != 'function') { errlog('Oh dear - invalid print function for a navlinkTag, id=' + this.id); } else { html = this.print(this); if (typeof html != typeof '') { html = ''; } else if (typeof this.shortcut != 'undefined') html = addPopupShortcut(html, this.shortcut); }		return opening + html + closing; };

navlinkTag.prototype.getNewWin = function { getValueOf('popupLinksNewWindow'); if (typeof pg.option.popupLinksNewWindow[this.id] === 'undefined') { this.newWin = null; }		this.newWin = pg.option.popupLinksNewWindow[this.id]; };

navlinkTag.prototype.getPrintFunction = function { //think about this some more // this.id and this.article should already be defined if (typeof this.id != typeof '' || typeof this.article != typeof {}) { return; }

this.noPopup = 1; switch (this.id) { case 'contribs': case 'history': case 'whatLinksHere': case 'userPage': case 'monobook': case 'userTalk': case 'talk': case 'article': case 'lastEdit': this.noPopup = null; }		switch (this.id) { case 'email': case 'contribs': case 'block': case 'unblock': case 'userlog': case 'userSpace': case 'deletedContribs': this.article = this.article.userName; }

switch (this.id) { case 'userTalk': case 'newUserTalk': case 'editUserTalk': case 'userPage': case 'monobook': case 'editMonobook': case 'blocklog': this.article = this.article.userName(true); /* fall through */ case 'pagelog': case 'deletelog': case 'protectlog': delete this.oldid; }

if (this.id == 'editMonobook' || this.id == 'monobook') { this.article.append('/monobook.js'); }

if (this.id != 'mainlink') { // FIXME anchor handling should be done differently with Title object this.article = this.article.removeAnchor; // if (typeof this.text=='undefined') this.text=popupString(this.id); }

switch (this.id) { case 'undelete': this.print = specialLink; this.specialpage = 'Undelete'; this.sep = '/'; break; case 'whatLinksHere': this.print = specialLink; this.specialpage = 'Whatlinkshere'; break; case 'relatedChanges': this.print = specialLink; this.specialpage = 'Recentchangeslinked'; break; case 'move': this.print = specialLink; this.specialpage = 'Movepage'; break; case 'contribs': this.print = specialLink; this.specialpage = 'Contributions'; break; case 'deletedContribs': this.print = specialLink; this.specialpage = 'Deletedcontributions'; break; case 'email': this.print = specialLink; this.specialpage = 'EmailUser'; this.sep = '/'; break; case 'block': this.print = specialLink; this.specialpage = 'Blockip'; this.sep = '&ip='; break; case 'unblock': this.print = specialLink; this.specialpage = 'Ipblocklist'; this.sep = '&action=unblock&ip='; break; case 'userlog': this.print = specialLink; this.specialpage = 'Log'; this.sep = '&user='; break; case 'blocklog': this.print = specialLink; this.specialpage = 'Log'; this.sep = '&type=block&page='; break; case 'pagelog': this.print = specialLink; this.specialpage = 'Log'; this.sep = '&page='; break; case 'protectlog': this.print = specialLink; this.specialpage = 'Log'; this.sep = '&type=protect&page='; break; case 'deletelog': this.print = specialLink; this.specialpage = 'Log'; this.sep = '&type=delete&page='; break; case 'userSpace': this.print = specialLink; this.specialpage = 'PrefixIndex'; this.sep = '&namespace=2&prefix='; break; case 'search': this.print = specialLink; this.specialpage = 'Search'; this.sep = '&fulltext=Search&search='; break; case 'thank': this.print = specialLink; this.specialpage = 'Thanks'; this.sep = '/'; this.article.value = this.diff !== 'prev' ? this.diff : this.oldid; break; case 'unwatch': case 'watch': this.print = magicWatchLink; this.action = this.id + '&autowatchlist=1&autoimpl=' + popupString('autoedit_version') + '&actoken=' + autoClickToken; break; case 'history': case 'historyfeed': case 'unprotect': case 'protect': this.print = wikiLink; this.action = this.id; break;

case 'delete': this.print = wikiLink; this.action = 'delete'; if (this.article.namespaceId == pg.nsImageId) { var img = this.article.stripNamespace; this.action += '&image=' + img; }				break;

case 'markpatrolled': case 'edit': // editOld should keep the oldid, but edit should not. delete this.oldid; /* fall through */ case 'view': case 'purge': case 'render': this.print = wikiLink; this.action = this.id; break; case 'raw': this.print = wikiLink; this.action = 'raw'; break; case 'new': this.print = wikiLink; this.action = 'edit&section=new'; break; case 'mainlink': if (typeof this.text == 'undefined') { this.text = this.article.toString.entify; }				if (getValueOf('popupSimplifyMainLink') && isInStrippableNamespace(this.article)) { // only show the /subpage part of the title text var s = this.text.split('/'); this.text = s[s.length - 1]; if (this.text === '' && s.length > 1) { this.text = s[s.length - 2]; }				}				this.print = titledWikiLink; if (					typeof this.title === 'undefined' &&					pg.current.link &&					typeof pg.current.link.href !== 'undefined'				) { this.title = safeDecodeURI(						pg.current.link.originalTitle ? pg.current.link.originalTitle : this.article					); if (typeof this.oldid !== 'undefined' && this.oldid) { this.title = tprintf('Revision %s of %s', [this.oldid, this.title]); }				}				this.action = 'view'; break; case 'userPage': case 'article': case 'monobook': case 'editMonobook': case 'editArticle': delete this.oldid; //alert(this.id+'\n'+this.article + '\n'+ typeof this.article); this.article = this.article.articleFromTalkOrArticle; //alert(this.id+'\n'+this.article + '\n'+ typeof this.article); this.print = wikiLink; if (this.id.indexOf('edit') === 0) { this.action = 'edit'; } else { this.action = 'view'; }				break; case 'userTalk': case 'talk': this.article = this.article.talkPage; delete this.oldid; this.print = wikiLink; this.action = 'view'; break; case 'arin': this.print = arinLink; break; case 'count': this.print = editCounterLink; break; case 'google': this.print = googleLink; break; case 'editors': this.print = editorListLink; break; case 'globalsearch': this.print = globalSearchLink; break; case 'lastEdit': this.print = titledDiffLink; this.title = popupString('Show the last edit'); this.from = 'prev'; this.to = 'cur'; break; case 'oldEdit': this.print = titledDiffLink; this.title = popupString('Show the edit made to get revision') + ' ' + this.oldid; this.from = 'prev'; this.to = this.oldid; break; case 'editOld': this.print = wikiLink; this.action = 'edit'; break; case 'undo': this.print = wikiLink; this.action = 'edit&undo='; break; case 'revert': this.print = wikiLink; this.action = 'revert'; break; case 'nullEdit': this.print = wikiLink; this.action = 'nullEdit'; break; case 'diffCur': this.print = titledDiffLink; this.title = tprintf('Show changes since revision %s', [this.oldid]); this.from = this.oldid; this.to = 'cur'; break; case 'editUserTalk': case 'editTalk': delete this.oldid; this.article = this.article.talkPage; this.action = 'edit'; this.print = wikiLink; break; case 'newUserTalk': case 'newTalk': this.article = this.article.talkPage; this.action = 'edit&section=new'; this.print = wikiLink; break; case 'lastContrib': case 'sinceMe': this.print = magicHistoryLink; break; case 'togglePreviews': this.text = popupString(pg.option.simplePopups ? 'enable previews' : 'disable previews'); /* fall through */ case 'disablePopups': case 'purgePopups': this.print = popupMenuLink; break; default: this.print = function { return 'Unknown navlink type: ' + this.id + ''; };		}	};	//	// end navlinks //////////////////////////////////////////////////	//</NOLITE> // ENDFILE: navlinks.js

// STARTFILE: shortcutkeys.js	//<NOLITE> function popupHandleKeypress(evt) { var keyCode = window.event ? window.event.keyCode : evt.keyCode ? evt.keyCode : evt.which; if (!keyCode || !pg.current.link || !pg.current.link.navpopup) { return; }		if (keyCode == 27) { // escape killPopup; return false; // swallow keypress }

var letter = String.fromCharCode(keyCode); var links = pg.current.link.navpopup.mainDiv.getElementsByTagName('A'); var startLink = 0; var i, j;

if (popupHandleKeypress.lastPopupLinkSelected) { for (i = 0; i < links.length; ++i) { if (links[i] == popupHandleKeypress.lastPopupLinkSelected) { startLink = i;				} }		}		for (j = 0; j < links.length; ++j) { i = (startLink + j + 1) % links.length; if (links[i].getAttribute('popupkey') == letter) { if (evt && evt.preventDefault) evt.preventDefault; links[i].focus; popupHandleKeypress.lastPopupLinkSelected = links[i]; return false; // swallow keypress }		}

// pass keypress on		if (document.oldPopupOnkeypress) { return document.oldPopupOnkeypress(evt); }		return true; }

function addPopupShortcuts { if (document.onkeypress != popupHandleKeypress) { document.oldPopupOnkeypress = document.onkeypress; }		document.onkeypress = popupHandleKeypress; }

function rmPopupShortcuts { popupHandleKeypress.lastPopupLinkSelected = null; try { if (document.oldPopupOnkeypress && document.oldPopupOnkeypress == popupHandleKeypress) { // panic document.onkeypress = null; //function {}; return; }			document.onkeypress = document.oldPopupOnkeypress; } catch (nasties) { /* IE goes here */ }	}

function addLinkProperty(html, property) { // take "<a href=...>...</a> and add a property		// not sophisticated at all, easily broken		var i = html.indexOf('>');		if (i < 0) {			return html;		}		return html.substring(0, i) + ' ' + property + html.substring(i);	}

function addPopupShortcut(html, key) { if (!getValueOf('popupShortcutKeys')) { return html; }		var ret = addLinkProperty(html, 'popupkey="' + key + '"'); if (key == ' ') { key = popupString('spacebar'); }		return ret.replace(RegExp('^(.*?)(title=")(.*?)(".*)$', 'i'), '$1$2$3 [' + key + ']$4'); }	//</NOLITE> // ENDFILE: shortcutkeys.js

// STARTFILE: diffpreview.js	//<NOLITE> //lets jump through hoops to find the rev ids we need to retrieve function loadDiff(article, oldid, diff, navpop) { navpop.diffData = { oldRev: {}, newRev: {} }; mw.loader.using('mediawiki.api').then(function {			var api = getMwApi;			var params = {				action: 'compare',				prop: 'ids|title',			};			if (article.title) {				params.fromtitle = article.title;			}

switch (diff) { case 'cur': switch (oldid) { case null: case '': case 'prev': // this can only work if we have the title // cur -> prev params.torelative = 'prev'; break; default: params.fromrev = oldid; params.torelative = 'cur'; break; }					break; case 'prev': if (oldid) { params.fromrev = oldid; } else { params.fromtitle; }					params.torelative = 'prev'; break; case 'next': params.fromrev = oldid || 0; params.torelative = 'next'; break; default: params.fromrev = oldid || 0; params.torev = diff || 0; break; }

api.get(params).then(function (data) {				navpop.diffData.oldRev.revid = data.compare.fromrevid;				navpop.diffData.newRev.revid = data.compare.torevid;

addReviewLink(navpop, 'popupMiscTools');

var go = function { pendingNavpopTask(navpop); var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';

url += 'revids=' + navpop.diffData.oldRev.revid + '|' + navpop.diffData.newRev.revid; url += '&prop=revisions&rvprop=ids|timestamp|content';

getPageWithCaching(url, doneDiff, navpop);

return true; // remove hook once run };				if (navpop.visible || !getValueOf('popupLazyDownloads')) { go; } else { navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_DIFFS'); }			});		});	}

// Put a "mark patrolled" link to an element target // TODO: Allow patrol a revision, as well as a diff function addReviewLink(navpop, target) { if (!pg.user.canReview) return; // If 'newRev' is older than 'oldRev' than it could be confusing, so we do not show the review link. if (navpop.diffData.newRev.revid <= navpop.diffData.oldRev.revid) return; var params = { action: 'query', prop: 'info|flagged', revids: navpop.diffData.oldRev.revid, formatversion: 2, };		getMwApi .get(params) .then(function (data) {				var stable_revid =					(data.query.pages[0].flagged && data.query.pages[0].flagged.stable_revid) || 0;				// The diff can be reviewed if the old version is the last reviewed version				// TODO: Other possible conditions that we may want to implement instead of this one:				// * old version is patrolled and the new version is not patrolled				//  * old version is patrolled and the new version is more recent than the last reviewed version				if (stable_revid == navpop.diffData.oldRev.revid) {					var a = document.createElement('a');					a.innerHTML = popupString('mark patrolled');					a.title = popupString('markpatrolledHint');					a.onclick = function  {						var params = {							action: 'review',							revid: navpop.diffData.newRev.revid,							comment: tprintf('defaultpopupReviewedSummary', [ navpop.diffData.oldRev.revid, navpop.diffData.newRev.revid, ]),						};						getMwApi							.postWithToken('csrf', params)							.done(function { a.style.display = 'none'; // TODO: Update current page and other already constructed popups })							.fail(function { alert(popupString('Could not marked this edit as patrolled')); });					};					setPopupHTML(a, target, navpop.idNumber, null, true);				}			}); }

function doneDiff(download) { if (!download.owner || !download.owner.diffData) { return; }		var navpop = download.owner; completedNavpopTask(navpop);

var pages, revisions = []; try { // Process the downloads pages = getJsObj(download.data).query.pages; for (var i = 0; i < pages.length; i++) { revisions = revisions.concat(pages[i].revisions); }			for (i = 0; i < revisions.length; i++) { if (revisions[i].revid == navpop.diffData.oldRev.revid) { navpop.diffData.oldRev.revision = revisions[i]; } else if (revisions[i].revid == navpop.diffData.newRev.revid) { navpop.diffData.newRev.revision = revisions[i]; }			}		} catch (someError) { errlog('Could not get diff'); }

insertDiff(navpop); }

function rmBoringLines(a, b, context) { if (typeof context == 'undefined') { context = 2; }		// this is fairly slow... i think it's quicker than doing a word-based diff from the off, though var aa = [], aaa = []; var bb = [], bbb = []; var i, j;

// first, gather all disconnected nodes in a and all crossing nodes in a and b		for (i = 0; i < a.length; ++i) { if (!a[i].paired) { aa[i] = 1; } else if (countCrossings(b, a, i, true)) { aa[i] = 1; bb[a[i].row] = 1; }		}

// pick up remaining disconnected nodes in b		for (i = 0; i < b.length; ++i) { if (bb[i] == 1) { continue; }			if (!b[i].paired) { bb[i] = 1; }		}

// another pass to gather context: we want the neighbours of included nodes which are not // yet included we have to add in partners of these nodes, but we don't want to add context // for *those* nodes in the next pass for (i = 0; i < b.length; ++i) { if (bb[i] == 1) { for (j = Math.max(0, i - context); j < Math.min(b.length, i + context); ++j) { if (!bb[j]) { bb[j] = 1; aa[b[j].row] = 0.5; }				}			}		}

for (i = 0; i < a.length; ++i) { if (aa[i] == 1) { for (j = Math.max(0, i - context); j < Math.min(a.length, i + context); ++j) { if (!aa[j]) { aa[j] = 1; bb[a[j].row] = 0.5; }				}			}		}

for (i = 0; i < bb.length; ++i) { if (bb[i] > 0) { // it's a row we need if (b[i].paired) { bbb.push(b[i].text); } // joined; partner should be in aa				else { bbb.push(b[i]); }			}		}		for (i = 0; i < aa.length; ++i) { if (aa[i] > 0) { // it's a row we need if (a[i].paired) { aaa.push(a[i].text); } // joined; partner should be in aa				else { aaa.push(a[i]); }			}		}

return { a: aaa, b: bbb }; }

function stripOuterCommonLines(a, b, context) { var i = 0; while (i < a.length && i < b.length && a[i] == b[i]) { ++i; }		var j = a.length - 1; var k = b.length - 1; while (j >= 0 && k >= 0 && a[j] == b[k]) { --j; --k; }

return { a: a.slice(Math.max(0, i - 1 - context), Math.min(a.length + 1, j + context + 1)), b: b.slice(Math.max(0, i - 1 - context), Math.min(b.length + 1, k + context + 1)), };	}

function insertDiff(navpop) { // for speed reasons, we first do a line-based diff, discard stuff that seems boring, then // do a word-based diff // FIXME: sometimes this gives misleading diffs as distant chunks are squashed together var oldlines = navpop.diffData.oldRev.revision.content.split('\n'); var newlines = navpop.diffData.newRev.revision.content.split('\n'); var inner = stripOuterCommonLines(oldlines, newlines, getValueOf('popupDiffContextLines')); oldlines = inner.a;		newlines = inner.b;		var truncated = false; getValueOf('popupDiffMaxLines'); if (			oldlines.length > pg.option.popupDiffMaxLines ||			newlines.length > pg.option.popupDiffMaxLines		) { // truncate truncated = true; inner = stripOuterCommonLines(				oldlines.slice(0, pg.option.popupDiffMaxLines),				newlines.slice(0, pg.option.popupDiffMaxLines),				pg.option.popupDiffContextLines			); oldlines = inner.a;			newlines = inner.b;		}

var lineDiff = diff(oldlines, newlines); var lines2 = rmBoringLines(lineDiff.o, lineDiff.n); var oldlines2 = lines2.a;		var newlines2 = lines2.b;

var simpleSplit = !String.prototype.parenSplit.isNative; var html = ' '; if (getValueOf('popupDiffDates')) { html += diffDatesTable(navpop); html += ' '; }		html += shortenDiffString(			diffString(oldlines2.join('\n'), newlines2.join('\n'), simpleSplit),			getValueOf('popupDiffContextCharacters')		).join(' '); setPopupTipsAndHTML(			html.split('\n').join(' ') +				(truncated ? ' ' + popupString('Diff truncated for performance reasons') + '' : ''),			'popupPreview',			navpop.idNumber		); }

function diffDatesTable(navpop) { var html = ' '; return html; }	function diffDatesTableRow(revision, label) { var txt = ''; var lastModifiedDate = new Date(revision.timestamp);

txt = formattedDateTime(lastModifiedDate);

var revlink = generalLink({			url: mw.config.get('wgScript') + '?oldid=' + revision.revid,			text: label,			title: label,		}); return simplePrintf(' %s  %s  ', [revlink, txt]); }	//</NOLITE> // ENDFILE: diffpreview.js

// STARTFILE: links.js	//<NOLITE> /////////////////////	// LINK GENERATION // /////////////////////

// titledDiffLink --> titledWikiLink --> generalLink // wikiLink	  --> titledWikiLink --> generalLink // editCounterLink --> generalLink

// TODO Make these functions return Element objects, not just raw HTML strings.

function titledDiffLink(l) { // article, text, title, from, to) {		return titledWikiLink({ article: l.article, action: l.to + '&oldid=' + l.from, newWin: l.newWin, noPopup: l.noPopup, text: l.text, title: l.title, /* hack: no oldid here */ actionName: 'diff', });	}

function wikiLink(l) { //{article:article, action:action, text:text, oldid, newid}) {		if ( !(typeof l.article == typeof {} && typeof l.action == typeof  && typeof l.text == typeof ) )			return null;		if (typeof l.oldid == 'undefined') {			l.oldid = null;		}		var savedOldid = l.oldid;		if (!/^(edit|view|revert|render)$|^raw/.test(l.action)) {			l.oldid = null;		}		var hint = popupString(l.action + 'Hint'); // revertHint etc etc etc		var oldidData = [l.oldid, safeDecodeURI(l.article)];		var revisionString = tprintf('revision %s of %s', oldidData);		log('revisionString=' + revisionString);		switch (l.action) {			case 'edit&section=new':				hint = popupString('newSectionHint');				break;			case 'edit&undo=':				if (l.diff && l.diff != 'prev' && savedOldid) {					l.action += l.diff + '&undoafter=' + savedOldid;				} else if (savedOldid) {					l.action += savedOldid;				}				hint = popupString('undoHint');				break;			case 'raw&ctype=text/css':				hint = popupString('rawHint');				break;			case 'revert':				var p = parseParams(pg.current.link.href);				l.action =					'edit&autoclick=wpSave&actoken=' + autoClickToken + '&autoimpl=' + popupString('autoedit_version') + '&autosummary=' + revertSummary(l.oldid, p.diff); if (p.diff == 'prev') { l.action += '&direction=prev'; revisionString = tprintf('the revision prior to revision %s of %s', oldidData); }				if (getValueOf('popupRevertSummaryPrompt')) { l.action += '&autosummaryprompt=true'; }				if (getValueOf('popupMinorReverts')) { l.action += '&autominor=true'; }				log('revisionString is now ' + revisionString); break; case 'nullEdit': l.action = 'edit&autoclick=wpSave&actoken=' + autoClickToken + '&autoimpl=' + popupString('autoedit_version') + '&autosummary=null'; break; case 'historyfeed': l.action = 'history&feed=rss'; break; case 'markpatrolled': l.action = 'markpatrolled&rcid=' + l.rcid; }

if (hint) { if (l.oldid) { hint = simplePrintf(hint, [revisionString]); } else { hint = simplePrintf(hint, [safeDecodeURI(l.article)]); }		} else { hint = safeDecodeURI(l.article + '&action=' + l.action) + l.oldid ? '&oldid=' + l.oldid : ''; }

return titledWikiLink({			article: l.article,			action: l.action,			text: l.text,			newWin: l.newWin,			title: hint,			oldid: l.oldid,			noPopup: l.noPopup,			onclick: l.onclick,		}); }

function revertSummary(oldid, diff) { var ret = ''; if (diff == 'prev') { ret = getValueOf('popupQueriedRevertToPreviousSummary'); } else { ret = getValueOf('popupQueriedRevertSummary'); }		return ret + '&autorv=' + oldid; }

function titledWikiLink(l) { // possible properties of argument: // article, action, text, title, oldid, actionName, className, noPopup // oldid = null is fine here

// article and action are mandatory args

if (typeof l.article == 'undefined' || typeof l.action == 'undefined') { errlog('got undefined article or action in titledWikiLink'); return null; }

var base = pg.wiki.titlebase + l.article.urlString; var url = base;

if (typeof l.actionName == 'undefined' || !l.actionName) { l.actionName = 'action'; }

// no need to add &action=view, and this confuses anchors if (l.action != 'view') { url = base + '&' + l.actionName + '=' + l.action; }

if (typeof l.oldid != 'undefined' && l.oldid) { url += '&oldid=' + l.oldid; }

var cssClass = pg.misc.defaultNavlinkClassname; if (typeof l.className != 'undefined' && l.className) { cssClass = l.className; }

return generalNavLink({			url: url,			newWin: l.newWin,			title: typeof l.title != 'undefined' ? l.title : null,			text: typeof l.text != 'undefined' ? l.text : null,			className: cssClass,			noPopup: l.noPopup,			onclick: l.onclick,		}); }

pg.fn.getLastContrib = function getLastContrib(wikipage, newWin) { getHistoryInfo(wikipage, function (x) {			processLastContribInfo(x, { page: wikipage, newWin: newWin });		}); };

function processLastContribInfo(info, stuff) { if (!info.edits || !info.edits.length) { alert('Popups: an odd thing happened. Please retry.'); return; }		if (!info.firstNewEditor) { alert(				tprintf('Only found one editor: %s made %s edits', [ info.edits[0].editor, info.edits.length, ])			);			return; }		var newUrl = pg.wiki.titlebase + new Title(stuff.page).urlString + '&diff=cur&oldid=' + info.firstNewEditor.oldid; displayUrl(newUrl, stuff.newWin); }

pg.fn.getDiffSinceMyEdit = function getDiffSinceMyEdit(wikipage, newWin) { getHistoryInfo(wikipage, function (x) {			processDiffSinceMyEdit(x, { page: wikipage, newWin: newWin });		}); };

function processDiffSinceMyEdit(info, stuff) { if (!info.edits || !info.edits.length) { alert('Popups: something fishy happened. Please try again.'); return; }		var friendlyName = stuff.page.split('_').join(' '); if (!info.myLastEdit) { alert(				tprintf("Couldn't find an edit by %s\nin the last %s edits to\n%s", [ info.userName, getValueOf('popupHistoryLimit'), friendlyName, ])			);			return; }		if (info.myLastEdit.index === 0) { alert(				tprintf('%s seems to be the last editor to the page %s', [info.userName, friendlyName])			); return; }		var newUrl = pg.wiki.titlebase + new Title(stuff.page).urlString + '&diff=cur&oldid=' + info.myLastEdit.oldid; displayUrl(newUrl, stuff.newWin); }

function displayUrl(url, newWin) { if (newWin) { window.open(url); } else { document.location = url; }	}

pg.fn.purgePopups = function purgePopups { processAllPopups(true); setupCache; // deletes all cached items (not browser cached, though...) pg.option = {}; abortAllDownloads; };

function processAllPopups(nullify, banish) { for (var i = 0; pg.current.links && i < pg.current.links.length; ++i) { if (!pg.current.links[i].navpopup) { continue; }			if (nullify || banish) pg.current.links[i].navpopup.banish; pg.current.links[i].simpleNoMore = false; if (nullify) pg.current.links[i].navpopup = null; }	}

pg.fn.disablePopups = function disablePopups { processAllPopups(false, true); setupTooltips(null, true); };

pg.fn.togglePreviews = function togglePreviews { processAllPopups(true, true); pg.option.simplePopups = !pg.option.simplePopups; abortAllDownloads; };

function magicWatchLink(l) { //Yuck!! Would require a thorough redesign to add this as a click event though ... l.onclick = simplePrintf("pg.fn.modifyWatchlist('%s','%s');return false;", [			l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),			this.id,		]); return wikiLink(l); }

pg.fn.modifyWatchlist = function modifyWatchlist(title, action) { var reqData = { action: 'watch', formatversion: 2, titles: title, uselang: mw.config.get('wgUserLanguage'), };		if (action === 'unwatch') reqData.unwatch = true;

// Load the Addedwatchtext or Removedwatchtext message and show it		var mwTitle = mw.Title.newFromText(title); var messageName; if (mwTitle && mwTitle.getNamespaceId > 0 && mwTitle.getNamespaceId % 2 === 1) { messageName = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk'; } else { messageName = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext'; }		$.when(			getMwApi.postWithToken('watch', reqData),			mw.loader.using(['mediawiki.api', 'mediawiki.jqueryMsg']).then(function { return api.loadMessagesIfMissing([messageName]); })		).done(function {			mw.notify(mw.message(messageName, title).parseDom);		}); };

function magicHistoryLink(l) { // FIXME use onclick change href trick to sort this out instead of window.open

var jsUrl = '', title = '', onClick = ''; switch (l.id) { case 'lastContrib': onClick = simplePrintf("pg.fn.getLastContrib('%s',%s)", [					l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),					l.newWin,				]); title = popupString('lastContribHint'); break; case 'sinceMe': onClick = simplePrintf("pg.fn.getDiffSinceMyEdit('%s',%s)", [					l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),					l.newWin,				]); title = popupString('sinceMeHint'); break; }		jsUrl = 'javascript:' + onClick; // jshint ignore:line onClick += ';return false;';

return generalNavLink({			url: jsUrl,			newWin: false, // can't have new windows with JS links, I think			title: title,			text: l.text,			noPopup: l.noPopup,			onclick: onClick,		}); }

function popupMenuLink(l) { var jsUrl = simplePrintf('javascript:pg.fn.%s', [l.id]); // jshint ignore:line var title = popupString(simplePrintf('%sHint', [l.id])); var onClick = simplePrintf('pg.fn.%s;return false;', [l.id]); return generalNavLink({			url: jsUrl,			newWin: false,			title: title,			text: l.text,			noPopup: l.noPopup,			onclick: onClick,		}); }

function specialLink(l) { // properties: article, specialpage, text, sep if (typeof l.specialpage == 'undefined' || !l.specialpage) return null; var base = pg.wiki.titlebase + mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':' +			l.specialpage; if (typeof l.sep == 'undefined' || l.sep === null) l.sep = '&target='; var article = l.article.urlString({			keepSpaces: l.specialpage == 'Search',		}); var hint = popupString(l.specialpage + 'Hint'); switch (l.specialpage) { case 'Log': switch (l.sep) { case '&user=': hint = popupString('userLogHint'); break; case '&type=block&page=': hint = popupString('blockLogHint'); break; case '&page=': hint = popupString('pageLogHint'); break; case '&type=protect&page=': hint = popupString('protectLogHint'); break; case '&type=delete&page=': hint = popupString('deleteLogHint'); break; default: log('Unknown log type, sep=' + l.sep); hint = 'Missing hint (FIXME)'; }				break; case 'PrefixIndex': article += '/'; break; }		if (hint) hint = simplePrintf(hint, [safeDecodeURI(l.article)]); else hint = safeDecodeURI(l.specialpage + ':' + l.article);

var url = base + l.sep + article; return generalNavLink({			url: url,			title: hint,			text: l.text,			newWin: l.newWin,			noPopup: l.noPopup,		}); }

function generalLink(l) { // l.url, l.text, l.title, l.newWin, l.className, l.noPopup, l.onclick if (typeof l.url == 'undefined') return null;

// only quotation marks in the url can screw us up now... I think var url = l.url.split('"').join('%22');

var ret = '<a href="' + url + '"'; if (typeof l.title != 'undefined' && l.title) { ret += ' title="' + pg.escapeQuotesHTML(l.title) + '"'; }		if (typeof l.onclick != 'undefined' && l.onclick) { ret += ' onclick="' + pg.escapeQuotesHTML(l.onclick) + '"'; }		if (l.noPopup) { ret += ' noPopup=1'; }		var newWin; if (typeof l.newWin == 'undefined' || l.newWin === null) { newWin = getValueOf('popupNewWindows'); } else { newWin = l.newWin; }		if (newWin) { ret += ' target="_blank"'; }		if (typeof l.className != 'undefined' && l.className) { ret += ' class="' + l.className + '"'; }		ret += '>'; if (typeof l.text == typeof '') { // We need to HTML-escape this to avoid XSS, but we also want to // display any existing HTML entities correctly, so unescape it first. // For example, the display text of the user page menu item is defined // as "user page", so we need to unescape first to avoid it being // escaped to "user&amp;nbsp;page". ret += pg.escapeQuotesHTML(pg.unescapeQuotesHTML(l.text)); }		ret += '</a>'; return ret; }

function appendParamsToLink(linkstr, params) { var sp = linkstr.parenSplit(RegExp('(href="[^"]+?)"', 'i'));		if (sp.length < 2) return null;		var ret = sp.shift + sp.shift;		ret += '&' + params + '"'; ret += sp.join(''); return ret; }

function changeLinkTargetLink(x) { // newTarget, text, hint, summary, clickButton, minor, title (optional), alsoChangeLabel { if (x.newTarget) { log('changeLinkTargetLink: newTarget=' + x.newTarget); }		if (x.oldTarget !== decodeURIComponent(x.oldTarget)) { log('This might be an input problem: ' + x.oldTarget); }

// FIXME: first character of page title as well as namespace should be case insensitive // eg category:X1 and Category:X1 are equivalent // this'll break if charAt(0) is nasty var cA = mw.util.escapeRegExp(x.oldTarget); var chs = cA.charAt(0).toUpperCase; chs = '[' + chs + chs.toLowerCase + ']'; var currentArticleRegexBit = chs + cA.substring(1); currentArticleRegexBit = currentArticleRegexBit .split(RegExp('(?:[_ ]+|%20)', 'g')) .join('(?:[_ ]+|%20)') .split('\\(')			.join('(?:%28|\\') .split('\\)')			.join('(?:%29|\\))'); // why does this need to match encoded strings ? links in the document ? // leading and trailing space should be ignored, and anchor bits optional: currentArticleRegexBit = '\\s*(' + currentArticleRegexBit + '(?:#[^\\[\\|]*)?)\\s*'; // e.g. Computer (archaic) -> \s*([Cc]omputer[_ ](?:%2528|\archaic(?:%2528|\)))\s*

// autoedit=s~\[\[([Cc]ad)\]\]~$1~g;s~\[\[([Cc]AD)[|]~[[Computer-aided%20design|~g

var title = x.title || mw.config.get('wgPageName').split('_').join(' '); var lk = titledWikiLink({			article: new Title(title),			newWin: x.newWin,			action: 'edit',			text: x.text,			title: x.hint,			className: 'popup_change_title_link',		}); var cmd = ''; if (x.newTarget) { // escape '&' and other nasties var t = x.newTarget; var s = mw.util.escapeRegExp(x.newTarget); if (x.alsoChangeLabel) { cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~' + t + '~g;'; cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~~g;';				cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + '~g';			} else {				cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~$1~g;';				cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~~g;';				cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + '~g';			}		} else {			cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~$1~g;';			cmd += 's~\\[\\[' + currentArticleRegexBit + '[|](.*?)\\]\\]~$2~g';		}		// Build query		cmd = 'autoedit=' + encodeURIComponent(cmd);		cmd +=			'&autoclick=' +			encodeURIComponent(x.clickButton) +			'&actoken=' +			encodeURIComponent(autoClickToken);		cmd += x.minor === null ?  : '&autominor=' + encodeURIComponent(x.minor);		cmd += x.watch === null ?  : '&autowatch=' + encodeURIComponent(x.watch);		cmd += '&autosummary=' + encodeURIComponent(x.summary);		cmd += '&autoimpl=' + encodeURIComponent(popupString('autoedit_version'));		return appendParamsToLink(lk, cmd);	}

function redirLink(redirMatch, article) { // NB redirMatch is in wikiText var ret = '';

if (getValueOf('popupAppendRedirNavLinks') && getValueOf('popupNavLinks')) { ret += ' ';

if (getValueOf('popupFixRedirs') && typeof autoEdit != 'undefined' && autoEdit) { ret += popupString('Redirects to: (Fix ');				log('redirLink: newTarget=' + redirMatch);				ret += addPopupShortcut( changeLinkTargetLink({						newTarget: redirMatch,						text: popupString('target'),						hint: popupString('Fix this redirect, changing just the link target'),						summary: simplePrintf(getValueOf('popupFixRedirsSummary'), [ article.toString, redirMatch, ]),						oldTarget: article.toString,						clickButton: getValueOf('popupRedirAutoClick'),						minor: true,						watch: getValueOf('popupWatchRedirredPages'),					}), 'R'				);				ret += popupString(' or ');				ret += addPopupShortcut( changeLinkTargetLink({						newTarget: redirMatch,						text: popupString('target & label'),						hint: popupString('Fix this redirect, changing the link target and label'),						summary: simplePrintf(getValueOf('popupFixRedirsSummary'), [ article.toString, redirMatch, ]),						oldTarget: article.toString,						clickButton: getValueOf('popupRedirAutoClick'),						minor: true,						watch: getValueOf('popupWatchRedirredPages'),						alsoChangeLabel: true,					}), 'R'				);				ret += popupString(')'); } else ret += popupString('Redirects') + popupString(' to ');

return ret; } else { return (				' ' +				popupString('Redirects') +				popupString(' to ') +				titledWikiLink({ article: new Title.fromWikiText(redirMatch), action: 'view' /* FIXME: newWin */, text: safeDecodeURI(redirMatch), title: popupString('Bypass redirect'), })			);		}	}

function arinLink(l) { if (!saneLinkCheck(l)) { return null; }		if (!l.article.isIpUser || !pg.wiki.wikimedia) return null;

var uN = l.article.userName;

return generalNavLink({			url: 'http://ws.arin.net/cgi-bin/whois.pl?queryinput=' + encodeURIComponent(uN),			newWin: l.newWin,			title: tprintf('Look up %s in ARIN whois database', [uN]),			text: l.text,			noPopup: 1,		}); }

function toolDbName(cookieStyle) { var ret = mw.config.get('wgDBname'); if (!cookieStyle) { ret += '_p'; }		return ret; }

function saneLinkCheck(l) { if (typeof l.article != typeof {} || typeof l.text != typeof '') { return false; }		return true; }	function editCounterLink(l) { if (!saneLinkCheck(l)) return null; if (!pg.wiki.wikimedia) return null; var uN = l.article.userName; var tool = getValueOf('popupEditCounterTool'); var url; var defaultToolUrl = '//tools.wmflabs.org/supercount/index.php?user=$1&project=$2.$3';

switch (tool) { case 'custom': url = simplePrintf(getValueOf('popupEditCounterUrl'), [					encodeURIComponent(uN),					toolDbName,				]); break; case 'soxred': // no longer available case 'kate': // no longer available case 'interiot': // no longer available /* fall through */ case 'supercount': default: var theWiki = pg.wiki.hostname.split('.'); url = simplePrintf(defaultToolUrl, [encodeURIComponent(uN), theWiki[0], theWiki[1]]); }		return generalNavLink({			url: url,			title: tprintf('editCounterLinkHint', [uN]),			newWin: l.newWin,			text: l.text,			noPopup: 1,		}); }

function globalSearchLink(l) { if (!saneLinkCheck(l)) return null; // [PATCHED] Changed the base URL below. The original was specific to Wikipedias and was // blocked by Fandom's global filters.

var base = 'https://minecraft.fandom.com/wiki/Special:Search?fulltext=1&scope=cross-wiki&query='; var article = l.article.urlString({ keepSpaces: true });

return generalNavLink({			url: base + article,			newWin: l.newWin,			title: tprintf('globalSearchHint', [safeDecodeURI(l.article)]),			text: l.text,			noPopup: 1,		}); }

function googleLink(l) { if (!saneLinkCheck(l)) return null;

var base = 'https://www.google.com/search?q='; var article = l.article.urlString({ keepSpaces: true });

return generalNavLink({			url: base + '%22' + article + '%22',			newWin: l.newWin,			title: tprintf('googleSearchHint', [safeDecodeURI(l.article)]),			text: l.text,			noPopup: 1,		}); }

function editorListLink(l) { if (!saneLinkCheck(l)) return null; var article = l.article.articleFromTalkPage || l.article; var url = 'https://xtools.wmflabs.org/articleinfo/' + encodeURI(pg.wiki.hostname) + '/' +			article.urlString + '?uselang=' + mw.config.get('wgUserLanguage'); return generalNavLink({			url: url,			title: tprintf('editorListHint', [article]),			newWin: l.newWin,			text: l.text,			noPopup: 1,		}); }

function generalNavLink(l) { l.className = l.className === null ? 'popupNavLink' : l.className; return generalLink(l); }

//////////////////////////////////////////////////	// magic history links //

function getHistoryInfo(wikipage, whatNext) { log('getHistoryInfo'); getHistory(			wikipage,			whatNext				? function (d) {						whatNext(processHistory(d));				 }				: processHistory		); }

// FIXME eliminate pg.idNumber ... how? :-(

function getHistory(wikipage, onComplete) { log('getHistory'); var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&prop=revisions&titles=' + new Title(wikipage).urlString + '&rvlimit=' + getValueOf('popupHistoryLimit'); log('getHistory: url=' + url); return startDownload(url, pg.idNumber + 'history', onComplete); }

function processHistory(download) { var jsobj = getJsObj(download.data); try { var revisions = anyChild(jsobj.query.pages).revisions; var edits = []; for (var i = 0; i < revisions.length; ++i) { edits.push({ oldid: revisions[i].revid, editor: revisions[i].user }); }			log('processed ' + edits.length + ' edits'); return finishProcessHistory(edits, mw.config.get('wgUserName')); } catch (someError) { log('Something went wrong with JSON business'); return finishProcessHistory([]); }	}

function finishProcessHistory(edits, userName) { var histInfo = {};

histInfo.edits = edits; histInfo.userName = userName;

for (var i = 0; i < edits.length; ++i) { if (typeof histInfo.myLastEdit === 'undefined' && userName && edits[i].editor == userName) { histInfo.myLastEdit = { index: i,					oldid: edits[i].oldid, previd: i === 0 ? null : edits[i - 1].oldid, };			}			if (typeof histInfo.firstNewEditor === 'undefined' && edits[i].editor != edits[0].editor) { histInfo.firstNewEditor = { index: i,					oldid: edits[i].oldid, previd: i === 0 ? null : edits[i - 1].oldid, };			}		}		//pg.misc.historyInfo=histInfo; return histInfo; }	//</NOLITE> // ENDFILE: links.js

// STARTFILE: options.js	////////////////////////////////////////////////// // options

// check for existing value, else use default function defaultize(x) { if (pg.option[x] === null || typeof pg.option[x] == 'undefined') { if (typeof window[x] != 'undefined') pg.option[x] = window[x]; else pg.option[x] = pg.optionDefault[x]; }	}

function newOption(x, def) { pg.optionDefault[x] = def; }

function setDefault(x, def) { return newOption(x, def); }

function getValueOf(varName) { defaultize(varName); return pg.option[varName]; }

/*eslint-disable */ function useDefaultOptions { // for testing for (var p in pg.optionDefault) { pg.option[p] = pg.optionDefault[p]; if (typeof window[p] != 'undefined') { delete window[p]; }		}	}	/*eslint-enable */

function setOptions { // user-settable parameters and defaults var userIsSysop = false; if (mw.config.get('wgUserGroups')) { for (var g = 0; g < mw.config.get('wgUserGroups').length; ++g) { if (mw.config.get('wgUserGroups')[g] == 'sysop') userIsSysop = true; }		}

// Basic options newOption('popupDelay', 0.5); newOption('popupHideDelay', 0.5); newOption('simplePopups', false); newOption('popupStructure', 'shortmenus'); // see later - default for popupStructure is 'original' if simplePopups is true newOption('popupActionsMenu', true); newOption('popupSetupMenu', true); newOption('popupAdminLinks', userIsSysop); newOption('popupShortcutKeys', false); newOption('popupHistoricalLinks', true); newOption('popupOnlyArticleLinks', true); newOption('removeTitles', true); newOption('popupMaxWidth', 350); newOption('popupSimplifyMainLink', true); newOption('popupAppendRedirNavLinks', true); newOption('popupTocLinks', false); newOption('popupSubpopups', true); newOption('popupDragHandle', false /* 'popupTopLinks'*/); newOption('popupLazyPreviews', true); newOption('popupLazyDownloads', true); newOption('popupAllDabsStubs', false); newOption('popupDebugging', false); newOption('popupActiveNavlinks', true); newOption('popupModifier', false); // ctrl, shift, alt or meta newOption('popupModifierAction', 'enable'); // or 'disable' newOption('popupDraggable', true); newOption('popupReview', false); newOption('popupLocale', false); newOption('popupDateTimeFormatterOptions', {			year: 'numeric',			month: 'long',			day: 'numeric',			hour12: false,			hour: '2-digit',			minute: '2-digit',			second: '2-digit',		}); newOption('popupDateFormatterOptions', {			year: 'numeric',			month: 'long',			day: 'numeric',		}); newOption('popupTimeFormatterOptions', {			hour12: false,			hour: '2-digit',			minute: '2-digit',			second: '2-digit',		});

//<NOLITE> // images newOption('popupImages', true); newOption('imagePopupsForImages', true); newOption('popupNeverGetThumbs', false); //newOption('popupImagesToggleSize',      true); newOption('popupThumbAction', 'imagepage'); //'sizetoggle');		newOption('popupImageSize', 60);		newOption('popupImageSizeLarge', 200);

// redirs, dabs, reversion newOption('popupFixRedirs', false); newOption('popupRedirAutoClick', 'wpDiff'); newOption('popupFixDabs', false); newOption('popupDabsAutoClick', 'wpDiff'); newOption('popupRevertSummaryPrompt', false); newOption('popupMinorReverts', false); newOption('popupRedlinkRemoval', false); newOption('popupRedlinkAutoClick', 'wpDiff'); newOption('popupWatchDisambiggedPages', null); newOption('popupWatchRedirredPages', null); newOption('popupDabWiktionary', 'last');

// navlinks newOption('popupNavLinks', true); newOption('popupNavLinkSeparator', ' &sdot; '); newOption('popupLastEditLink', true); newOption('popupEditCounterTool', 'supercount'); newOption('popupEditCounterUrl', ''); //</NOLITE>

// previews etc newOption('popupPreviews', true); newOption('popupSummaryData', true); newOption('popupMaxPreviewSentences', 5); newOption('popupMaxPreviewCharacters', 600); newOption('popupLastModified', true); newOption('popupPreviewKillTemplates', true); newOption('popupPreviewRawTemplates', true); newOption('popupPreviewFirstParOnly', true); newOption('popupPreviewCutHeadings', true); newOption('popupPreviewButton', false); newOption('popupPreviewButtonEvent', 'click');

//<NOLITE> // diffs newOption('popupPreviewDiffs', true); newOption('popupDiffMaxLines', 100); newOption('popupDiffContextLines', 2); newOption('popupDiffContextCharacters', 40); newOption('popupDiffDates', true); newOption('popupDiffDatePrinter', 'toLocaleString'); // no longer in use

// edit summaries. God, these are ugly. newOption('popupReviewedSummary', popupString('defaultpopupReviewedSummary')); newOption('popupFixDabsSummary', popupString('defaultpopupFixDabsSummary')); newOption('popupExtendedRevertSummary', popupString('defaultpopupExtendedRevertSummary')); newOption('popupRevertSummary', popupString('defaultpopupRevertSummary')); newOption('popupRevertToPreviousSummary', popupString('defaultpopupRevertToPreviousSummary')); newOption('popupQueriedRevertSummary', popupString('defaultpopupQueriedRevertSummary')); newOption(			'popupQueriedRevertToPreviousSummary',			popupString('defaultpopupQueriedRevertToPreviousSummary')		); newOption('popupFixRedirsSummary', popupString('defaultpopupFixRedirsSummary')); newOption('popupRedlinkSummary', popupString('defaultpopupRedlinkSummary')); newOption('popupRmDabLinkSummary', popupString('defaultpopupRmDabLinkSummary')); //</NOLITE> // misc newOption('popupHistoryLimit', 50); //<NOLITE> newOption('popupFilters', [			popupFilterStubDetect,			popupFilterDisambigDetect,			popupFilterPageSize,			popupFilterCountLinks,			popupFilterCountImages,			popupFilterCountCategories,			popupFilterLastModified,		]); newOption('extraPopupFilters', []); newOption('popupOnEditSelection', 'cursor'); newOption('popupPreviewHistory', true); newOption('popupImageLinks', true); newOption('popupCategoryMembers', true); newOption('popupUserInfo', true); newOption('popupHistoryPreviewLimit', 25); newOption('popupContribsPreviewLimit', 25); newOption('popupRevDelUrl', '//en.wikipedia.org/wiki/Wikipedia:Revision_deletion'); newOption('popupShowGender', true); //</NOLITE>

// new windows newOption('popupNewWindows', false); newOption('popupLinksNewWindow', { lastContrib: true, sinceMe: true });

// regexps newOption(			'popupDabRegexp',			'\\{\\{\\s*(d(ab|isamb(ig(uation)?)?)|(((geo|hn|road?|school|number)dis)|[234][lc][acw]|(road|ship)index))\\s*(\\|[^}]*)?\\}\\}|is a .*disambiguation.*page'		); newOption('popupAnchorRegexp', 'anchors?'); //how to identify an anchors template newOption('popupStubRegexp', '(sect)?stub[}][}]|This .*-related article is a .*stub'); newOption(			'popupImageVarsRegexp',			'image|image_(?:file|skyline|name|flag|seal)|cover|badge|logo'		); }	// ENDFILE: options.js

// STARTFILE: strings.js	//<NOLITE> //////////////////////////////////////////////////	// Translatable strings //////////////////////////////////////////////////	//	// See instructions at	// https://en.wikipedia.org/wiki/Wikipedia:Tools/Navigation_popups/Translation

pg.string = { /////////////////////////////////////		// summary data, searching etc.		///////////////////////////////////// article: 'article', category: 'category', categories: 'categories', image: 'image', images: 'images', stub: 'stub', 'section stub': 'section stub', 'Empty page': 'Empty page', kB: 'kB', bytes: 'bytes', day: 'day', days: 'days', hour: 'hour', hours: 'hours', minute: 'minute', minutes: 'minutes', second: 'second', seconds: 'seconds', week: 'week', weeks: 'weeks', search: 'search', SearchHint: 'Find English Wikipedia articles containing %s', web: 'web', global: 'global', globalSearchHint: 'Search across Wikipedias in different languages for %s', googleSearchHint: 'Google for %s', /////////////////////////////////////		// article-related actions and info // (some actions also apply to user pages) /////////////////////////////////////		actions: 'actions', ///// view articles and view talk popupsMenu: 'popups', togglePreviewsHint: 'Toggle preview generation in popups on this page', 'enable previews': 'enable previews', 'disable previews': 'disable previews', 'toggle previews': 'toggle previews', 'show preview': 'show preview', reset: 'reset', 'more...': 'more...', disable: 'disable popups', disablePopupsHint: 'Disable popups on this page. Reload page to re-enable.', historyfeedHint: 'RSS feed of recent changes to this page', purgePopupsHint: 'Reset popups, clearing all cached popup data.', PopupsHint: 'Reset popups, clearing all cached popup data.', spacebar: 'space', view: 'view', 'view article': 'view article', viewHint: 'Go to %s', talk: 'talk', 'talk page': 'talk page', 'this revision': 'this revision', 'revision %s of %s': 'revision %s of %s', 'Revision %s of %s': 'Revision %s of %s', 'the revision prior to revision %s of %s': 'the revision prior to revision %s of %s', 'Toggle image size': 'Click to toggle image size', del: 'del', ///// delete, protect, move delete: 'delete', deleteHint: 'Delete %s', undeleteShort: 'un', UndeleteHint: 'Show the deletion history for %s', protect: 'protect', protectHint: 'Restrict editing rights to %s', unprotectShort: 'un', unprotectHint: 'Allow %s to be edited by anyone again', 'send thanks': 'send thanks', ThanksHint: 'Send a thank you notification to this user', move: 'move', 'move page': 'move page', MovepageHint: 'Change the title of %s', edit: 'edit', ///// edit articles and talk 'edit article': 'edit article', editHint: 'Change the content of %s', 'edit talk': 'edit talk', new: 'new', 'new topic': 'new topic', newSectionHint: 'Start a new section on %s', 'null edit': 'null edit', nullEditHint: 'Submit an edit to %s, making no changes ', hist: 'hist', ///// history, diffs, editors, related history: 'history', historyHint: 'List the changes made to %s', last: 'prev', // For labelling the previous revision in history pages; the key is "last" for backwards compatibility lastEdit: 'lastEdit', 'mark patrolled': 'mark patrolled', markpatrolledHint: 'Mark this edit as patrolled', 'Could not marked this edit as patrolled': 'Could not marked this edit as patrolled', 'show last edit': 'most recent edit', 'Show the last edit': 'Show the effects of the most recent change', lastContrib: 'lastContrib', 'last set of edits': 'latest edits', lastContribHint: 'Show the net effect of changes made by the last editor', cur: 'cur', diffCur: 'diffCur', 'Show changes since revision %s': 'Show changes since revision %s', '%s old': '%s old', // as in 4 weeks old oldEdit: 'oldEdit', purge: 'purge', purgeHint: 'Demand a fresh copy of %s', raw: 'source', rawHint: 'Download the source of %s', render: 'simple', renderHint: 'Show a plain HTML version of %s', 'Show the edit made to get revision': 'Show the edit made to get revision', sinceMe: 'sinceMe', 'changes since mine': 'diff my edit', sinceMeHint: 'Show changes since my last edit', "Couldn't find an edit by %s\nin the last %s edits to\n%s": "Couldn't find an edit by %s\nin the last %s edits to\n%s", eds: 'eds', editors: 'editors', editorListHint: 'List the users who have edited %s', related: 'related', relatedChanges: 'relatedChanges', 'related changes': 'related changes', RecentchangeslinkedHint: 'Show changes in articles related to %s', editOld: 'editOld', ///// edit old version, or revert rv: 'rv', revert: 'revert', revertHint: 'Revert to %s', defaultpopupReviewedSummary: 'Accepted by reviewing the difference between this version and previously accepted version using popups', defaultpopupRedlinkSummary: 'Removing link to empty page %s using popups', defaultpopupFixDabsSummary: 'Disambiguate %s to %s using popups', defaultpopupFixRedirsSummary: 'Redirect bypass from %s to %s using popups', defaultpopupExtendedRevertSummary: 'Revert to revision dated %s by %s, oldid %s using popups', defaultpopupRevertToPreviousSummary: 'Revert to the revision prior to revision %s using popups', defaultpopupRevertSummary: 'Revert to revision %s using popups', defaultpopupQueriedRevertToPreviousSummary: 'Revert to the revision prior to revision $1 dated $2 by $3 using popups', defaultpopupQueriedRevertSummary: 'Revert to revision $1 dated $2 by $3 using popups', defaultpopupRmDabLinkSummary: 'Remove link to dab page %s using popups', Redirects: 'Redirects', // as in Redirects to ... ' to ': ' to ', // as in Redirects to ... 'Bypass redirect': 'Bypass redirect', 'Fix this redirect': 'Fix this redirect', disambig: 'disambig', ///// add or remove dab etc.		disambigHint: 'Disambiguate this link to %s', 'Click to disambiguate this link to:': 'Click to disambiguate this link to:', 'remove this link': 'remove this link', 'remove all links to this page from this article': 'remove all links to this page from this article', 'remove all links to this disambig page from this article': 'remove all links to this disambig page from this article', mainlink: 'mainlink', ///// links, watch, unwatch wikiLink: 'wikiLink', wikiLinks: 'wikiLinks', 'links here': 'links here', whatLinksHere: 'whatLinksHere', 'what links here': 'what links here', WhatlinkshereHint: 'List the pages that are hyperlinked to %s', unwatchShort: 'un', watchThingy: 'watch', // called watchThingy because {}.watch is a function watchHint: 'Add %s to my watchlist', unwatchHint: 'Remove %s from my watchlist', 'Only found one editor: %s made %s edits': 'Only found one editor: %s made %s edits', '%s seems to be the last editor to the page %s': '%s seems to be the last editor to the page %s', rss: 'rss', /////////////////////////////////////		// diff previews /////////////////////////////////////		'Diff truncated for performance reasons': 'Diff truncated for performance reasons', 'Old revision': 'Old revision', 'New revision': 'New revision', 'Something went wrong :-(': 'Something went wrong :-(', 'Empty revision, maybe non-existent': 'Empty revision, maybe non-existent', 'Unknown date': 'Unknown date', /////////////////////////////////////		// other special previews /////////////////////////////////////		'Empty category': 'Empty category', 'Category members (%s shown)': 'Category members (%s shown)', 'No image links found': 'No image links found', 'File links': 'File links', 'No image found': 'No image found', 'Image from Commons': 'Image from Commons', 'Description page': 'Description page', 'Alt text:': 'Alt text:', revdel: 'Hidden revision', /////////////////////////////////////		// user-related actions and info /////////////////////////////////////		user: 'user', ///// user page, talk, email, space 'user page': 'user page', 'user talk': 'user talk', 'edit user talk': 'edit user talk', 'leave comment': 'leave comment', email: 'email', 'email user': 'email user', EmailuserHint: 'Send an email to %s', space: 'space', // short form for userSpace link PrefixIndexHint: 'Show pages in the userspace of %s', count: 'count', ///// contributions, log 'edit counter': 'edit counter', editCounterLinkHint: 'Count the contributions made by %s', contribs: 'contribs', contributions: 'contributions', deletedContribs: 'deleted contributions', DeletedcontributionsHint: 'List deleted edits made by %s', ContributionsHint: 'List the contributions made by %s', log: 'log', 'user log': 'user log', userLogHint: "Show %s's user log", arin: 'ARIN lookup', ///// ARIN lookup, block user or IP		'Look up %s in ARIN whois database': 'Look up %s in the ARIN whois database', unblockShort: 'un', block: 'block', 'block user': 'block user', IpblocklistHint: 'Unblock %s', BlockipHint: 'Prevent %s from editing', 'block log': 'block log', blockLogHint: 'Show the block log for %s', protectLogHint: 'Show the protection log for %s', pageLogHint: 'Show the page log for %s', deleteLogHint: 'Show the deletion log for %s', 'Invalid %s %s': 'The option %s is invalid: %s', 'No backlinks found': 'No backlinks found', ' and more': ' and more', undo: 'undo', undoHint: 'undo this edit', 'Download preview data': 'Download preview data', 'Invalid or IP user': 'Invalid or IP user', 'Not a registered username': 'Not a registered username', BLOCKED: 'BLOCKED', 'Has blocks': 'Has blocks', ' edits since: ': ' edits since: ', 'last edit on ': 'last edit on ', 'he/him': 'he/him', 'she/her': 'she/her', /////////////////////////////////////		// Autoediting /////////////////////////////////////		'Enter a non-empty edit summary or press cancel to abort': 'Enter a non-empty edit summary or press cancel to abort', 'Failed to get revision information, please edit manually.\n\n': 'Failed to get revision information, please edit manually.\n\n', 'The %s button has been automatically clicked. Please wait for the next page to load.': 'The %s button has been automatically clicked. Please wait for the next page to load.', 'Could not find button %s. Please check the settings in your javascript file.': 'Could not find button %s. Please check the settings in your javascript file.', /////////////////////////////////////		// Popups setup /////////////////////////////////////		'Open full-size image': 'Open full-size image', zxy: 'zxy', autoedit_version: 'np20140416', };

function popupString(str) { if (typeof popupStrings != 'undefined' && popupStrings && popupStrings[str]) { return popupStrings[str]; }		if (pg.string[str]) { return pg.string[str]; }		return str; }

function tprintf(str, subs) { if (typeof subs != typeof []) { subs = [subs]; }		return simplePrintf(popupString(str), subs); }

//</NOLITE> // ENDFILE: strings.js

// STARTFILE: run.js	//////////////////////////////////////////////////////////////////// // Run things ////////////////////////////////////////////////////////////////////

// For some reason popups requires a fully loaded page jQuery.ready(...) causes problems for some. // The old addOnloadHook did something similar to the below if (document.readyState == 'complete') autoEdit; //will setup popups else $(window).on('load', autoEdit);

// Support for MediaWiki's live preview, VisualEditor's saves and Echo's flyout. (function {		var once = true;		function dynamicContentHandler($content) {			// Try to detect the hook fired on initial page load and disregard			// it, we already hook to onload (possibly to different parts of // page - it's configurable) and running twice might be bad. Ugly…			if ($content.attr('id') == 'mw-content-text') {				if (once) {					once = false;					return;				}			}

function registerHooksForVisibleNavpops { for (var i = 0; pg.current.links && i < pg.current.links.length; ++i) { var navpop = pg.current.links[i].navpopup; if (!navpop || !navpop.isVisible) { continue; }

Navpopup.tracker.addHook(posCheckerHook(navpop)); }			}

function doIt { registerHooksForVisibleNavpops; $content.each(function {					this.ranSetupTooltipsAlready = false;					setupTooltips(this);				}); }

setupPopups(doIt); }

// This hook is also fired after page load. mw.hook('wikipage.content').add(dynamicContentHandler);

mw.hook('ext.echo.overlay.beforeShowingOverlay').add(function ($overlay) {			dynamicContentHandler($overlay.find('.mw-echo-state'));		}); }); }); // ENDFILE: run.js //