MediaWiki:Gadget-popups.js

// STARTFILE: main.js

// **********************************************************************

// ** **

// ** changes to this file affect many users. **

// ** please discuss on the talk page before editing **

// ** **

// **********************************************************************

// ** **

// ** if you do edit this file, be sure that your editor recognizes it **

// ** as utf8, or the weird and wonderful characters in the namespaces **

// ** below will be completely broken. You can check with the show **

// ** changes button before submitting the edit. **

// ** test: مدیا מיוחד Мэдыя **

// ** **

// **********************************************************************

/* eslint-env browser */

/* global $, jQuery, mw, window */

// Fix later

/* global log, errlog, popupStrings, wikEdUseWikEd, WikEdUpdateFrame */

$(() => {

//////////////////////////////////////////////////

// Globals

//

// Trying to shove as many of these as possible into the pg (popup globals) object

const pg = {

api: {}, // MediaWiki API requests

re: {}, // regexps

ns: {}, // namespaces

string: {}, // translatable strings

wiki: {}, // local site info

user: {}, // current user info

misc: {}, // YUCK PHOOEY

option: {}, // options, see newOption etc

optionDefault: {}, // default option values

flag: {}, // misc flags

cache: {}, // page and image cache

structures: {}, // navlink structures

timer: {}, // all sorts of timers (too damn many)

counter: {}, // .. and all sorts of counters

current: {}, // state info

fn: {}, // functions

endoflist: null,

};

/* Bail if the gadget/script is being loaded twice */

/* An element with id "pg" would add a window.pg property, ignore such property */

if (window.pg && !(window.pg instanceof HTMLElement)) {

return;

}

/* Export to global context */

window.pg = pg;

/// Local Variables: ///

/// mode:c ///

/// End: ///

// ENDFILE: main.js

// STARTFILE: actions.js

function setupTooltips(container, remove, force, popData) {

log('setupTooltips, container=' + container + ', remove=' + remove);

if (!container) {

// the main initial call

if (

getValueOf('popupOnEditSelection') &&

document &&

document.editform &&

document.editform.wpTextbox1

) {

document.editform.wpTextbox1.onmouseup = doSelectionPopup;

}

// article/content is a structure-dependent thing

container = defaultPopupsContainer();

}

if (!remove && !force && container.ranSetupTooltipsAlready) {

return;

}

container.ranSetupTooltipsAlready = !remove;

let anchors;

anchors = container.getElementsByTagName('A');

setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);

}

function defaultPopupsContainer() {

if (getValueOf('popupOnlyArticleLinks')) {

return (

document.querySelector('.skin-vector-2022 .vector-body') ||

document.getElementById('mw_content') ||

document.getElementById('content') ||

document.getElementById('article') ||

document

);

}

return document;

}

function setupTooltipsLoop(anchors, begin, howmany, sleep, remove, popData) {

log(simplePrintf('setupTooltipsLoop(%s,%s,%s,%s,%s)', arguments));

const finish = begin + howmany;

const loopend = Math.min(finish, anchors.length);

let j = loopend - begin;

log(

'setupTooltips: anchors.length=' +

anchors.length +

', begin=' +

begin +

', howmany=' +

howmany +

', loopend=' +

loopend +

', remove=' +

remove

);

const doTooltip = remove ? removeTooltip : addTooltip;

// try a faster (?) loop construct

if (j > 0) {

do {

const a = anchors[loopend - j];

if (typeof a === 'undefined' || !a || !a.href) {

log('got null anchor at index ' + loopend - j);

continue;

}

doTooltip(a, popData);

} while (--j);

}

if (finish < anchors.length) {

setTimeout(() => {

setupTooltipsLoop(anchors, finish, howmany, sleep, remove, popData);

}, sleep);

} else {

if (!remove && !getValueOf('popupTocLinks')) {

rmTocTooltips();

}

pg.flag.finishedLoading = true;

}

}

// eliminate popups from the TOC

// This also kills any onclick stuff that used to be going on in the toc

function rmTocTooltips() {

const toc = document.getElementById('toc');

if (toc) {

const tocLinks = toc.getElementsByTagName('A');

const tocLen = tocLinks.length;

for (let j = 0; j < tocLen; ++j) {

removeTooltip(tocLinks[j], true);

}

}

}

function addTooltip(a, popData) {

if (!isPopupLink(a)) {

return;

}

a.onmouseover = mouseOverWikiLink;

a.onmouseout = mouseOutWikiLink;

a.onmousedown = killPopup;

a.hasPopup = true;

a.popData = popData;

}

function removeTooltip(a) {

if (!a.hasPopup) {

return;

}

a.onmouseover = null;

a.onmouseout = null;

if (a.originalTitle) {

a.title = a.originalTitle;

}

a.hasPopup = false;

}

function removeTitle(a) {

if (!a.originalTitle) {

a.originalTitle = a.title;

}

a.title = '';

}

function restoreTitle(a) {

if (a.title || !a.originalTitle) {

return;

}

a.title = a.originalTitle;

}

function registerHooks(np) {

const popupMaxWidth = getValueOf('popupMaxWidth');

if (typeof popupMaxWidth === 'number') {

const setMaxWidth = function () {

np.mainDiv.style.maxWidth = popupMaxWidth + 'px';

np.maxWidth = popupMaxWidth;

};

np.addHook(setMaxWidth, 'unhide', 'before');

}

np.addHook(addPopupShortcuts, 'unhide', 'after');

np.addHook(rmPopupShortcuts, 'hide', 'before');

}

function removeModifierKeyHandler(a) {

//remove listeners for modifier key if any that were added in mouseOverWikiLink

document.removeEventListener('keydown', a.modifierKeyHandler, false);

document.removeEventListener('keyup', a.modifierKeyHandler, false);

}

function mouseOverWikiLink(evt) {

if (!evt && window.event) {

evt = window.event;

}

// if the modifier is needed, listen for it,

// we will remove the listener when we mouseout of this link or kill popup.

if (getValueOf('popupModifier')) {

// if popupModifierAction = enable, we should popup when the modifier is pressed

// if popupModifierAction = disable, we should popup unless the modifier is pressed

const action = getValueOf('popupModifierAction');

const key = action == 'disable' ? 'keyup' : 'keydown';

const a = this;

a.modifierKeyHandler = function (evt) {

mouseOverWikiLink2(a, evt);

};

document.addEventListener(key, a.modifierKeyHandler, false);

}

return mouseOverWikiLink2(this, evt);

}

/**

* Gets the references list item that the provided footnote link targets. This

* is typically a li element within the ol.references element inside the reflist.

*

* @param {Element} a - A footnote link.

* @return {Element|boolean} The targeted element, or false if one can't be found.

*/

function footnoteTarget(a) {

const aTitle = Title.fromAnchor(a);

// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly

const anch = aTitle.anchor;

if (!/^(cite_note-|_note-|endnote)/.test(anch)) {

return false;

}

const lTitle = Title.fromURL(location.href);

if (lTitle.toString(true) !== aTitle.toString(true)) {

return false;

}

let el = document.getElementById(anch);

while (el && typeof el.nodeName === 'string') {

const nt = el.nodeName.toLowerCase();

if (nt === 'li') {

return el;

} else if (nt === 'body') {

return false;

} else if (el.parentNode) {

el = el.parentNode;

} else {

return false;

}

}

return false;

}

function footnotePreview(x, navpop) {

setPopupHTML('


' + x.innerHTML, 'popupPreview', navpop.idNumber);

}

function modifierPressed(evt) {

const mod = getValueOf('popupModifier');

if (!mod) {

return false;

}

if (!evt && window.event) {

evt = window.event;

}

return evt && mod && evt[mod.toLowerCase() + 'Key'];

}

// Checks if the correct modifier pressed/unpressed if needed

function isCorrectModifier(a, evt) {

if (!getValueOf('popupModifier')) {

return true;

}

// if popupModifierAction = enable, we should popup when the modifier is pressed

// if popupModifierAction = disable, we should popup unless the modifier is pressed

const action = getValueOf('popupModifierAction');

return (

(action == 'enable' && modifierPressed(evt)) || (action == 'disable' && !modifierPressed(evt))

);

}

function mouseOverWikiLink2(a, evt) {

if (!isCorrectModifier(a, evt)) {

return;

}

if (getValueOf('removeTitles')) {

removeTitle(a);

}

if (a == pg.current.link && a.navpopup && a.navpopup.isVisible()) {

return;

}

pg.current.link = a;

if (getValueOf('simplePopups') && !pg.option.popupStructure) {

// reset *default value* of popupStructure

setDefault('popupStructure', 'original');

}

const article = new Title().fromAnchor(a);

// set global variable (ugh) to hold article (wikipage)

pg.current.article = article;

if (!a.navpopup) {

a.navpopup = newNavpopup(a, article);

pg.current.linksHash[a.href] = a.navpopup;

pg.current.links.push(a);

}

if (a.navpopup.pending === null || a.navpopup.pending !== 0) {

// either fresh popups or those with unfinshed business are redone from scratch

simplePopupContent(a, article);

}

a.navpopup.showSoonIfStable(a.navpopup.delay);

clearInterval(pg.timer.checkPopupPosition);

pg.timer.checkPopupPosition = setInterval(checkPopupPosition, 600);

if (getValueOf('simplePopups')) {

if (getValueOf('popupPreviewButton') && !a.simpleNoMore) {

const d = document.createElement('div');

d.className = 'popupPreviewButtonDiv';

const s = document.createElement('span');

d.appendChild(s);

s.className = 'popupPreviewButton';

s['on' + getValueOf('popupPreviewButtonEvent')] = function () {

a.simpleNoMore = true;

d.style.display = 'none';

nonsimplePopupContent(a, article);

};

s.innerHTML = popupString('show preview');

setPopupHTML(d, 'popupPreview', a.navpopup.idNumber);

}

}

if (a.navpopup.pending !== 0) {

nonsimplePopupContent(a, article);

}

}

// simplePopupContent: the content that do not require additional download

// (it is shown even when simplePopups is true)

function simplePopupContent(a, article) {

/* FIXME hack */ a.navpopup.hasPopupMenu = false;

a.navpopup.setInnerHTML(popupHTML(a));

fillEmptySpans({ navpopup: a.navpopup });

if (getValueOf('popupDraggable')) {

let dragHandle = getValueOf('popupDragHandle') || null;

if (dragHandle && dragHandle != 'all') {

dragHandle += a.navpopup.idNumber;

}

setTimeout(() => {

a.navpopup.makeDraggable(dragHandle);

}, 150);

}

if (getValueOf('popupRedlinkRemoval') && a.className == 'new') {

setPopupHTML('
' + popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);

}

}

function debugData(navpopup) {

if (getValueOf('popupDebugging') && navpopup.idNumber) {

setPopupHTML(

'idNumber=' + navpopup.idNumber + ', pending=' + navpopup.pending,

'popupError',

navpopup.idNumber

);

}

}

function newNavpopup(a, article) {

const navpopup = new Navpopup();

navpopup.fuzz = 5;

navpopup.delay = getValueOf('popupDelay') * 1000;

// increment global counter now

navpopup.idNumber = ++pg.idNumber;

navpopup.parentAnchor = a;

navpopup.parentPopup = a.popData && a.popData.owner;

navpopup.article = article;

registerHooks(navpopup);

return navpopup;

}

// Should we show nonsimple context?

// If simplePopups is set to true, then we do not show nonsimple context,

// but if a bottom "show preview" was clicked we do show nonsimple context

function shouldShowNonSimple(a) {

return !getValueOf('simplePopups') || a.simpleNoMore;

}

// Should we show nonsimple context govern by the option (e.g. popupUserInfo)?

// If the user explicitly asked for nonsimple context by setting the option to true,

// then we show it even in nonsimple mode.

function shouldShow(a, option) {

if (shouldShowNonSimple(a)) {

return getValueOf(option);

} else {

return typeof window[option] != 'undefined' && window[option];

}

}

function nonsimplePopupContent(a, article) {

let diff = null,

history = null;

const params = parseParams(a.href);

const oldid = typeof params.oldid == 'undefined' ? null : params.oldid;

if (shouldShow(a, 'popupPreviewDiffs')) {

diff = params.diff;

}

if (shouldShow(a, 'popupPreviewHistory')) {

history = params.action == 'history';

}

a.navpopup.pending = 0;

const referenceElement = footnoteTarget(a);

if (referenceElement) {

footnotePreview(referenceElement, a.navpopup);

} else if (diff || diff === 0) {

loadDiff(article, oldid, diff, a.navpopup);

} else if (history) {

loadAPIPreview('history', article, a.navpopup);

} else if (shouldShowNonSimple(a) && pg.re.contribs.test(a.href)) {

loadAPIPreview('contribs', article, a.navpopup);

} else if (shouldShowNonSimple(a) && pg.re.backlinks.test(a.href)) {

loadAPIPreview('backlinks', article, a.navpopup);

} else if (

// FIXME should be able to get all preview combinations with options

article.namespaceId() == pg.nsImageId &&

(shouldShow(a, 'imagePopupsForImages') || !anchorContainsImage(a))

) {

loadAPIPreview('imagepagepreview', article, a.navpopup);

loadImage(article, a.navpopup);

} else {

if (article.namespaceId() == pg.nsCategoryId && shouldShow(a, 'popupCategoryMembers')) {

loadAPIPreview('category', article, a.navpopup);

} else if (

(article.namespaceId() == pg.nsUserId || article.namespaceId() == pg.nsUsertalkId) &&

shouldShow(a, 'popupUserInfo')

) {

loadAPIPreview('userinfo', article, a.navpopup);

}

if (shouldShowNonSimple(a)) {

startArticlePreview(article, oldid, a.navpopup);

}

}

}

function pendingNavpopTask(navpop) {

if (navpop && navpop.pending === null) {

navpop.pending = 0;

}

++navpop.pending;

debugData(navpop);

}

function completedNavpopTask(navpop) {

if (navpop && navpop.pending) {

--navpop.pending;

}

debugData(navpop);

}

function startArticlePreview(article, oldid, navpop) {

navpop.redir = 0;

loadPreview(article, oldid, navpop);

}

function loadPreview(article, oldid, navpop) {

if (!navpop.redir) {

navpop.originalArticle = article;

}

article.oldid = oldid;

loadAPIPreview('revision', article, navpop);

}

function loadPreviewFromRedir(redirMatch, navpop) {

// redirMatch is a regex match

const target = new Title().fromWikiText(redirMatch[2]);

// overwrite (or add) anchor from original target

// mediawiki does overwrite; eg User:Lupin/foo3#Done

if (navpop.article.anchor) {

target.anchor = navpop.article.anchor;

}

navpop.redir++;

navpop.redirTarget = target;

const warnRedir = redirLink(target, navpop.article);

setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);

navpop.article = target;

fillEmptySpans({ redir: true, redirTarget: target, navpopup: navpop });

return loadPreview(target, null, navpop);

}

function insertPreview(download) {

if (!download.owner) {

return;

}

const redirMatch = pg.re.redirect.exec(download.data);

if (download.owner.redir === 0 && redirMatch) {

loadPreviewFromRedir(redirMatch, download.owner);

return;

}

if (download.owner.visible || !getValueOf('popupLazyPreviews')) {

insertPreviewNow(download);

} else {

const id = download.owner.redir ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';

download.owner.addHook(

() => {

insertPreviewNow(download);

return true;

},

'unhide',

'after',

id

);

}

}

function insertPreviewNow(download) {

if (!download.owner) {

return;

}

const wikiText = download.data;

const navpop = download.owner;

const art = navpop.redirTarget || navpop.originalArticle;

makeFixDabs(wikiText, navpop);

if (getValueOf('popupSummaryData')) {

getPageInfo(wikiText, download);

setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);

}

let imagePage = '';

if (art.namespaceId() == pg.nsImageId) {

imagePage = art.toString();

} else {

imagePage = getValidImageFromWikiText(wikiText);

}

if (imagePage) {

loadImage(Title.fromWikiText(imagePage), navpop);

}

if (getValueOf('popupPreviews')) {

insertArticlePreview(download, art, navpop);

}

}

function insertArticlePreview(download, art, navpop) {

if (download && typeof download.data == typeof '') {

if (art.namespaceId() == pg.nsTemplateId && getValueOf('popupPreviewRawTemplates')) {

// FIXME compare/consolidate with diff escaping code for wikitext

const h =

'


' +

download.data.entify().split('\\n').join('
\\n') +

'';

setPopupHTML(h, 'popupPreview', navpop.idNumber);

} else {

const p = prepPreviewmaker(download.data, art, navpop);

p.showPreview();

}

}

}

function prepPreviewmaker(data, article, navpop) {

// deal with tricksy anchors

const d = anchorize(data, article.anchorString());

const urlBase = joinPath([pg.wiki.articlebase, article.urlString()]);

const p = new Previewmaker(d, urlBase, navpop);

return p;

}

// Try to imitate the way mediawiki generates HTML anchors from section titles

function anchorize(d, anch) {

if (!anch) {

return d;

}

const anchRe = RegExp(

'(?:=+\\s*' +

literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') +

'\\s*=+|\\{\\{\\s*' +

getValueOf('popupAnchorRegexp') +

'\\s*(?:\\|[^|}]*)*?\\s*' +

literalizeRegex(anch) +

'\\s*(?:\\|[^}]*)?}})'

);

const match = d.match(anchRe);

if (match && match.length > 0 && match[0]) {

return d.substring(d.indexOf(match[0]));

}

// now try to deal with == foo baz boom == -> #foo_baz_boom

const lines = d.split('\n');

for (let i = 0; i < lines.length; ++i) {

lines[i] = lines[i]

.replace(/\*?[|])?(.*?)[\]]{2}/g, '$2')

.replace(/'''([^'])/g, '$1')

.replace(/''([^'])/g, '$1');

if (lines[i].match(anchRe)) {

return d.split('\n').slice(i).join('\n').replace(/^[^=]*/, '');

}

}

return d;

}

function killPopup() {

removeModifierKeyHandler(this);

if (getValueOf('popupShortcutKeys')) {

rmPopupShortcuts();

}

if (!pg) {

return;

}

if (pg.current.link && pg.current.link.navpopup) {

pg.current.link.navpopup.banish();

}

pg.current.link = null;

abortAllDownloads();

if (pg.timer.checkPopupPosition) {

clearInterval(pg.timer.checkPopupPosition);

pg.timer.checkPopupPosition = null;

}

return true; // preserve default action

}

// ENDFILE: actions.js

// STARTFILE: domdrag.js

/**

* @file

* The {@link Drag} object, which enables objects to be dragged around.

*

*

* *************************************************

* dom-drag.js

* 09.25.2001

* www.youngpup.net

* **************************************************

* 10.28.2001 - fixed minor bug where events

* sometimes fired off the handle, not the root.

* *************************************************

* Pared down, some hooks added by User:Lupin

*

* Copyright Aaron Boodman.

* Saying stupid things daily since March 2001.

*

*/

/**

* Creates a new Drag object. This is used to make various DOM elements draggable.

*

* @constructor

*/

function Drag() {

/**

* Condition to determine whether or not to drag. This function should take one parameter,

* an Event. To disable this, set it to null.

*

* @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 null.

*

* @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 {HTMLElement} o The "handle" by which oRoot is dragged.

* @param {HTMLElement} oRoot The object which moves when o is dragged, or o if omitted.

*/

Drag.prototype.init = function (o, oRoot) {

const 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 || 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) {

const o = this.obj; // = this;

e = this.fixE(e);

if (this.startCondition && !this.startCondition(e)) {

return;

}

const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);

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

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

const o = this.obj;

const ey = e.clientY;

const ex = e.clientX;

const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);

const x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);

let 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 (const prop in pg.structures[oldStructure]) {

pg.structures[newStructure][prop] = pg.structures[oldStructure][prop];

}

}

copyStructure('original', 'nostalgia');

pg.structures.nostalgia.popupTopLinks = function (x) {

let 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

const editstr = '<>';

const editOldidStr =

'if(oldid){<>|<>|<>}else{' +

editstr +

'}';

const historystr = '<>';

const watchstr = '<>|<>';

str +=

'
if(talk){' +

editOldidStr +

'|<>' +

'*' +

historystr +

'*' +

watchstr +

'*' +

'<>|<>' +

'}else{' + // not a talk page

editOldidStr +

'*' +

historystr +

'*' +

watchstr +

'*' +

'<>|<>|<>}';

// misc links

str += '
<>*<>';

str += 'if(admin){
}else{*}<>';

// admin links

str +=

'if(admin){*<>|<>*' +

'<>|<>}';

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('<>', x.article, x.params);

};

pg.structures.fancy.popupTopLinks = function (x) {

const hist =

'<>|<>|<>';

const watch = '<>|<>';

const move = '<>';

return navlinkStringToHTML(

'if(talk){' +

'<>|<>*' +

hist +

'*' +

'<>|<>' +

'*' +

watch +

'*' +

move +

'}else{<>*' +

hist +

'*<>|<>|<>' +

'*' +

watch +

'*' +

move +

'}
',

x.article,

x.params

);

};

pg.structures.fancy.popupOtherLinks = function (x) {

const admin =

'<>|<>*<>|<>';

let user = '<>if(wikimedia){|<>}';

user +=

'if(ipuser){|<>}else{*<

popupString('email') +

'>>}if(admin){*<>}';

const normal = '<>*<>';

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(/
$/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

const s = [];

const dropclass = 'popup_drop';

const enddiv = '

';

let hist = '<>';

if (!shorter) {

hist = '' + hist + '|<>|<>';

}

const lastedit = '<>';

const thank = 'if(diff){<>}';

const jsHistory = '<><>';

const linkshere = '<>';

const related = '<>';

const search =

'<>if(wikimedia){|<>}' +

'|<>';

const watch = '<>|<>';

const protect =

'<>|' +

'<>|<>';

const del =

'<>|<>|<>';

const move = '<>';

const nullPurge = '<>|<>';

const viewOptions = '<>|<>|<>';

const editRow =

'if(oldid){' +

'<>|<>' +

'<>|<>' +

'}else{<>}';

const markPatrolled = 'if(rcid){<>}';

const newTopic = 'if(talk){<>}';

const protectDelete = 'if(admin){' + protect + del + '}';

if (getValueOf('popupActionsMenu')) {

s.push('<>*' + menuTitle(dropclass, 'actions'));

} else {

s.push('

<>');

}

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){<><>}' +

'else{<><>' +

'<>}

' +

enddiv

);

// user menu starts here

const email = '<>';

const contribs =

'if(wikimedia){}<>if(wikimedia){}' +

'if(admin){<>}';

s.push('if(user){*' + menuTitle(dropclass, 'user'));

s.push('

');

s.push('<>|<>');

s.push(

'<><>' +

'<>'

);

if (!shorter) {

s.push('if(ipuser){<>}else{' + email + '}');

} else {

s.push('if(ipuser){}else{' + email + '}');

}

s.push('


' + contribs + '<>');

s.push('if(wikimedia){<>}');

s.push(

'if(admin){<>|<>}'

);

s.push('<>');

s.push('

' + enddiv + '}');

// popups menu starts here

if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) {

x.navpop.hasPopupMenu = true;

s.push('*' + menuTitle(dropclass, 'popupsMenu') + '

');

s.push('<>');

s.push('<>');

s.push('<>');

s.push('

' + enddiv);

}

return navlinkStringToHTML(s.join(''), x.article, x.params);

};

function menuTitle(dropclass, s) {

const text = popupString(s); // i18n

const len = text.length;

return '

' + text + '';

}

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;

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 '

' + x.article.toString() + '
';

};

// ENDFILE: structures.js

// STARTFILE: autoedit.js

function substitute(data, cmdBody) {

// alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags);

const fromRe = RegExp(cmdBody.from, cmdBody.flags);

return data.replace(fromRe, cmdBody.to);

}

function execCmds(data, cmdList) {

for (let 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 [];

}

let 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

let from, to, flags, tmp;

if (str.length < 4) {

return false;

}

const 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) {

const endSegment = findNext(str, sep);

if (endSegment < 0) {

return false;

}

const 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 (let 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) {

const 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(() => {

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;

const cmdString = mw.util.getParamValue('autoedit');

if (cmdString) {

try {

const editbox = document.editform.wpTextbox1;

const cmdList = parseCmd(cmdString);

const input = editbox.value;

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

const rvid = mw.util.getParamValue('autorv');

if (rvid) {

const url =

pg.wiki.apiwikibase +

'?action=query&format=json&formatversion=2&prop=revisions&revids=' +

rvid;

startDownload(url, null, autoEdit2);

} else {

autoEdit2();

}

});

}

function autoEdit2(d) {

let summary = mw.util.getParamValue('autosummary');

let summaryprompt = mw.util.getParamValue('autosummaryprompt');

let summarynotice = '';

if (d && d.data && mw.util.getParamValue('autorv')) {

const 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) {

const txt =

summarynotice + popupString('Enter a non-empty edit summary or press cancel to abort');

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

}

const btn = mw.util.getParamValue('autoclick');

if (btn) {

if (document.editform && document.editform[btn]) {

const button = document.editform[btn];

const 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) {

const headings = document.getElementsByTagName('h1');

if (headings) {

const div = document.createElement('div');

div.innerHTML = '' + pg.escapeQuotesHTML(s) + '';

headings[0].parentNode.insertBefore(div, headings[0]);

}

}

function getRvSummary(template, json) {

try {

const o = getJsObj(json);

const edit = anyChild(o.query.pages).revisions[0];

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

}

}

// ENDFILE: autoedit.js

// STARTFILE: downloader.js

/**

* @file

* {@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 {number}

*/

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 abort

*

* @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 aborted 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 null on failure.

*

* @return {Date}

*/

Downloader.prototype.getLastModifiedDate = function () {

if (!this.http) {

return null;

}

let 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 f(this) 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 {number} 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) {

const d = new Downloader(url);

if (!d.http) {

return 'ohdear';

}

d.id = id;

d.setTarget();

if (!onfailure) {

onfailure = 2;

}

const 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 {number} id The ID.

* @param {Function} callback The callback, which is invoked immediately as callback(d),

* where d 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) {

const 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 {number} 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) {

const 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 (const 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)

*/

const 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.locale.user +

':' +

Insta.conf.user.name +

'|' +

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) {

let 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() {

let 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, '&').replace(//g, '>');

}

// 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(/

.replace(/>/g, '>')

.replace(/:/g, ':')

.replace(/\[/g, '[')

.replace(/]/g, ']');

}

function htmlescape_attr(s) {

return htmlescape_text(s).replace(/'/g, ''').replace(/"/g, '"');

}

// 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() {

let prev = '';

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

const l_match = r;

sh();

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

// close uncontinued lists

for (let prevPos = prev.length - 1; prevPos >= ipos; prevPos--) {

const pi = prev.charAt(prevPos);

if (pi == '*') {

ps('');

} else if (pi == '#') {

ps('');

}

// close a dl only if the new item is not a dl item (:, ; or empty)

else if ($.inArray(l_match[1].charAt(prevPos), ['', '*', '#'])) {

ps('');

}

}

// open new lists

for (let matchPos = ipos; matchPos < l_match[1].length; matchPos++) {

const li = l_match[1].charAt(matchPos);

if (li == '*') {

ps('

');

}

function navlinkDepth(magic, s) {

return s.split('<' + magic + '>').length - s.split('').length;

}

// navlinkString: * becomes the separator

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

const p = navlinkStringToArray(s, article, params);

let html = '';

let menudepth = 0; // nested menus not currently allowed, but doesn't do any harm to code for it

let menurowdepth = 0;

for (let 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 += '

';

} else {

html += p[i].html();

}

}

}

return html;

}

function navlinkTag() {

this.type = 'navlinkTag';

}

navlinkTag.prototype.html = function () {

this.getNewWin();

this.getPrintFunction();

let html = '';

let opening, closing;

const tagType = 'span';

if (!tagType) {

opening = '';

closing = '';

} else {

opening = '<' + tagType + ' class="popup_' + this.id + '">';

closing = '';

}

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) {

const 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§ion=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

const 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§ion=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: ' + String(this.id);

};

}

};

//

// end navlinks

//////////////////////////////////////////////////

// ENDFILE: navlinks.js

// STARTFILE: shortcutkeys.js

function popupHandleKeypress(evt) {

const 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

}

const letter = String.fromCharCode(keyCode);

const links = pg.current.link.navpopup.mainDiv.getElementsByTagName('A');

let startLink = 0;

let 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 "... and add a property

// not sophisticated at all, easily broken

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

}

const ret = addLinkProperty(html, 'popupkey="' + key + '"');

if (key == ' ') {

key = popupString('spacebar');

}

return ret.replace(/^(.*?)(title=")(.*?)(".*)$/i, '$1$2$3 [' + key + ']$4');

}

// ENDFILE: shortcutkeys.js

// STARTFILE: diffpreview.js

/**

* Load diff data.

*

* lets jump through hoops to find the rev ids we need to retrieve

*

* @param {Title} article

* @param {string} oldid

* @param {string} diff

* @param {Navpopup} navpop

*/

function loadDiff(article, oldid, diff, navpop) {

navpop.diffData = { oldRev: {}, newRev: {} };

mw.loader.using('mediawiki.api').then(() => {

const api = getMwApi();

const params = {

action: 'compare',

prop: 'ids|title',

};

params.fromtitle = article.toString();

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 && oldid !== 'cur') {

params.fromrev = oldid;

}

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((data) => {

navpop.diffData.oldRev.revid = data.compare.fromrevid;

navpop.diffData.newRev.revid = data.compare.torevid;

addReviewLink(navpop, 'popupMiscTools');

const go = function () {

pendingNavpopTask(navpop);

let url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';

url += 'revids=' + navpop.diffData.oldRev.revid + '|' + navpop.diffData.newRev.revid;

url += '&prop=revisions&rvslots=main&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;

}

const params = {

action: 'query',

prop: 'info|flagged',

revids: navpop.diffData.oldRev.revid,

formatversion: 2,

};

getMwApi()

.get(params)

.then((data) => {

const 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) {

const a = document.createElement('a');

a.innerHTML = popupString('mark patrolled');

a.title = popupString('markpatrolledHint');

a.onclick = function () {

const params = {

action: 'review',

revid: navpop.diffData.newRev.revid,

comment: tprintf('defaultpopupReviewedSummary', [

navpop.diffData.oldRev.revid,

navpop.diffData.newRev.revid,

]),

};

getMwApi()

.postWithToken('csrf', params)

.done(() => {

a.style.display = 'none';

// TODO: Update current page and other already constructed popups

})

.fail(() => {

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;

}

const navpop = download.owner;

completedNavpopTask(navpop);

let 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

const aa = [],

aaa = [];

const bb = [],

bbb = [];

let 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) {

let i = 0;

while (i < a.length && i < b.length && a[i] == b[i]) {

++i;

}

let j = a.length - 1;

let 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

let oldlines = navpop.diffData.oldRev.revision.slots.main.content.split('\n');

let newlines = navpop.diffData.newRev.revision.slots.main.content.split('\n');

let inner = stripOuterCommonLines(oldlines, newlines, getValueOf('popupDiffContextLines'));

oldlines = inner.a;

newlines = inner.b;

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

}

const lineDiff = diff(oldlines, newlines);

const lines2 = rmBoringLines(lineDiff.o, lineDiff.n);

const oldlines2 = lines2.a;

const newlines2 = lines2.b;

const simpleSplit = !String.prototype.parenSplit.isNative;

let 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) {

let html = '

';

html += diffDatesTableRow(navpop.diffData.newRev.revision, tprintf('New revision'));

html += diffDatesTableRow(navpop.diffData.oldRev.revision, tprintf('Old revision'));

html += '

';

return html;

}

function diffDatesTableRow(revision, label) {

let txt = '';

const lastModifiedDate = new Date(revision.timestamp);

txt = formattedDateTime(lastModifiedDate);

const revlink = generalLink({

url: mw.config.get('wgScript') + '?oldid=' + revision.revid,

text: label,

title: label,

});

return simplePrintf('%s%s', [revlink, txt]);

}

// ENDFILE: diffpreview.js

// STARTFILE: links.js

/////////////////////

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

}

const savedOldid = l.oldid;

if (!/^(edit|view|revert|render)$|^raw/.test(l.action)) {

l.oldid = null;

}

let hint = popupString(l.action + 'Hint'); // revertHint etc etc etc

const oldidData = [l.oldid, safeDecodeURI(l.article)];

let revisionString = tprintf('revision %s of %s', oldidData);

log('revisionString=' + revisionString);

switch (l.action) {

case 'edit§ion=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) {

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

}

const base = pg.wiki.titlebase + l.article.urlString();

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

}

let 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, (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;

}

const 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, (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;

}

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

}

const 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 (let 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) {

const 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

const mwTitle = mw.Title.newFromText(title);

let 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),

getMwApi().loadMessagesIfMissing([messageName])

).done(() => {

mw.notify(mw.message(messageName, title).parseDom());

});

};

function magicHistoryLink(l) {

// FIXME use onclick change href trick to sort this out instead of window.open

let 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) {

const jsUrl = simplePrintf('javascript:pg.fn.%s()', [l.id]); // jshint ignore:line

const title = popupString(simplePrintf('%sHint', [l.id]));

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

}

const base =

pg.wiki.titlebase +

mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +

':' +

l.specialpage;

if (typeof l.sep == 'undefined' || l.sep === null) {

l.sep = '&target=';

}

let article = l.article.urlString({

keepSpaces: l.specialpage == 'Search',

});

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

}

const url = base + l.sep + article;

return generalNavLink({

url: url,

title: hint,

text: l.text,

newWin: l.newWin,

noPopup: l.noPopup,

});

}

/**

* Builds a link from a object representing a link

*

* @param {Object} link

* @param {string} link.url URL

* @param {string} link.text The text to show for a link

* @param {string} link.title Title of the link, this shows up

* when you hover over the link

* @param {boolean} link.newWin Should open in a new Window

* @param {number} link.noPopup Should nest new popups from link (0 or 1)

* @param {string} link.onclick

* @return {string|null} null if no url is given

*/

function generalLink(link) {

if (typeof link.url == 'undefined') {

return null;

}

const elem = document.createElement( 'a' );

elem.href = link.url;

elem.title = link.title;

// The onclick event adds raw JS in textual form to the HTML.

// TODO: We should look into removing this, and/or auditing what gets sent.

elem.setAttribute( 'onclick', link.onclick );

if ( link.noPopup ) {

elem.setAttribute('noPopup', '1' );

}

let newWin;

if (typeof link.newWin == 'undefined' || link.newWin === null) {

newWin = getValueOf('popupNewWindows');

} else {

newWin = link.newWin;

}

if (newWin) {

elem.target = '_blank';

}

if (link.className) {

elem.className = link.className;

}

elem.innerText = pg.unescapeQuotesHTML(link.text);

return elem.outerHTML;

}

function appendParamsToLink(linkstr, params) {

const sp = linkstr.parenSplit(/(href="[^"]+?)"/i);

if (sp.length < 2) {

return null;

}

let 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

const cA = mw.util.escapeRegExp(x.oldTarget);

let chs = cA.charAt(0).toUpperCase();

chs = '[' + chs + chs.toLowerCase() + ']';

let currentArticleRegexBit = chs + cA.substring(1);

currentArticleRegexBit = currentArticleRegexBit

.split(/(?:[_ ]+|%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

const title = x.title || mw.config.get('wgPageName').split('_').join(' ');

const lk = titledWikiLink({

article: new Title(title),

newWin: x.newWin,

action: 'edit',

text: x.text,

title: x.hint,

className: 'popup_change_title_link',

});

let cmd = '';

if (x.newTarget) {

// escape '&' and other nasties

const t = x.newTarget;

const s = mw.util.escapeRegExp(x.newTarget);

if (x.alsoChangeLabel) {

cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~' + t + '~g;';

cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~g;';

cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~' + t + '~g';

} else {

cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~$1~g;';

cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~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

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

}

const 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) {

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

}

const uN = l.article.userName();

const tool = getValueOf('popupEditCounterTool');

let url;

const defaultToolUrl = 'https://xtools.wmflabs.org/ec?user=$1&project=$2.$3&uselang=' + mw.config.get('wgUserLanguage');

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;

}

const base = 'https://global-search.toolforge.org/?uselang=' + mw.config.get('wgUserLanguage') + '&q=';

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

}

const base = 'https://www.google.com/search?q=';

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

}

const article = l.article.articleFromTalkPage() || l.article;

const 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 ?

(d) => {

whatNext(processHistory(d));

} :

processHistory

);

}

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

function getHistory(wikipage, onComplete) {

log('getHistory');

const 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) {

const jsobj = getJsObj(download.data);

try {

const revisions = anyChild(jsobj.query.pages).revisions;

const edits = [];

for (let 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) {

const histInfo = {};

histInfo.edits = edits;

histInfo.userName = userName;

for (let 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;

}

// 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

let userIsSysop = false;

if (mw.config.get('wgUserGroups')) {

for (let 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',

});

// 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', ' ⋅ ');

newOption('popupLastEditLink', true);

newOption('popupEditCounterTool', 'supercount');

newOption('popupEditCounterUrl', '');

// 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');

// 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'));

// misc

newOption('popupHistoryLimit', 50);

newOption('popupFilters', [

popupFilterStubDetect,

popupFilterDisambigDetect,

popupFilterPageSize,

popupFilterCountLinks,

popupFilterCountImages,

popupFilterCountCategories,

popupFilterLastModified,

popupFilterWikibaseItem,

]);

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

// new windows

newOption('popupNewWindows', false);

newOption('popupLinksNewWindow', { lastContrib: true, sinceMe: true });

// regexps

newOption(

'popupDabRegexp',

'disambiguation\\}\\}|\\{\\{\\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

//////////////////////////////////////////////////

// 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',

'History preview failed': 'History preview failed :-(',

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

}

// 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 () {

let 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 (let i = 0; pg.current.links && i < pg.current.links.length; ++i) {

const 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(($overlay) => {

dynamicContentHandler($overlay.find('.mw-echo-state'));

});

}());

});

// ENDFILE: run.js