MediaWiki:Gadget-popups.js: Unterschied zwischen den Versionen

Aus SnotWiki
Zur Navigation springen Zur Suche springen
 
Zeile 14: Zeile 14:
 
// **                                                                  **
 
// **                                                                  **
 
// **********************************************************************
 
// **********************************************************************
 +
/* eslint-env browser  */
 +
/* global $, jQuery, mw, window */
 +
 +
// Fix later
 +
/* global log, errlog, popupStrings, wikEdUseWikEd, WikEdUpdateFrame */
 +
/* eslint no-mixed-spaces-and-tabs: 0, no-empty: 0 */
  
 
$(function () {
 
$(function () {
//////////////////////////////////////////////////
+
//////////////////////////////////////////////////
// Globals
+
// Globals
//
+
//
 +
 
 +
// Trying to shove as many of these as possible into the pg (popup globals) object
 +
var 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;
 +
}
  
// Trying to shove as many of these as possible into the pg (popup globals) object
+
/* Export to global context */
var pg = {
+
window.pg = pg;
re: {},              // regexps
 
ns: {},              // namespaces
 
string: {},          // translatable strings
 
wiki: {},            // local site 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
 
};
 
var pop = {               // wrap various functions in here
 
init: {},
 
util: {},
 
endoflist: null
 
};
 
/* Export to global context */
 
window.pg = pg;
 
  
function popupsReady() {
+
/// Local Variables: ///
if (!pg.flag) { return false; }
+
/// mode:c ///
if (!pg.flag.finishedLoading) { return false; }
+
/// End: ///
return true;
+
// ENDFILE: main.js
}
 
  
/// Local Variables: ///
+
// STARTFILE: actions.js
/// mode:c ///
+
function setupTooltips(container, remove, force, popData) {
/// End: ///
+
log('setupTooltips, container=' + container + ', remove=' + remove);
// ENDFILE: main.js
+
if (!container) {
// STARTFILE: actions.js
+
//<NOLITE>
function setupTooltips(container, remove, force, popData) {
+
// the main initial call
log('setupTooltips, container='+container+', remove='+remove);
+
if (
if (!container) {
+
getValueOf('popupOnEditSelection') &&
//<NOLITE>
+
document &&
// the main initial call
+
document.editform &&
if (getValueOf('popupOnEditSelection') && document && document.editform && document.editform.wpTextbox1) {
+
document.editform.wpTextbox1
document.editform.wpTextbox1.onmouseup=doSelectionPopup;
+
) {
 +
document.editform.wpTextbox1.onmouseup = doSelectionPopup;
 +
}
 +
//</NOLITE>
 +
// article/content is a structure-dependent thing
 +
container = defaultPopupsContainer();
 
}
 
}
//</NOLITE>
 
// article/content is a structure-dependent thing
 
container = defaultPopupsContainer();
 
}
 
  
if (!remove && !force && container.ranSetupTooltipsAlready) { return; }
+
if (!remove && !force && container.ranSetupTooltipsAlready) {
container.ranSetupTooltipsAlready = !remove;
+
return;
 +
}
 +
container.ranSetupTooltipsAlready = !remove;
  
var anchors;
+
var anchors;
anchors=container.getElementsByTagName('A');
+
anchors = container.getElementsByTagName('A');
setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);
+
setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);
}
+
}
  
function defaultPopupsContainer() {
+
function defaultPopupsContainer() {
if (getValueOf('popupOnlyArticleLinks')) {
+
if (getValueOf('popupOnlyArticleLinks')) {
return document.getElementById('mw_content') ||  
+
return (
document.getElementById('content') ||
+
document.getElementById('mw_content') ||
document.getElementById('article') || document;
+
document.getElementById('content') ||
 +
document.getElementById('article') ||
 +
document
 +
);
 +
}
 +
return document;
 
}
 
}
return  document;
 
}
 
  
function setupTooltipsLoop(anchors,begin,howmany,sleep, remove, popData) {
+
function setupTooltipsLoop(anchors, begin, howmany, sleep, remove, popData) {
log(simplePrintf('setupTooltipsLoop(%s,%s,%s,%s,%s)', arguments));
+
log(simplePrintf('setupTooltipsLoop(%s,%s,%s,%s,%s)', arguments));
var finish=begin+howmany;
+
var finish = begin + howmany;
var loopend = min(finish, anchors.length);
+
var loopend = Math.min(finish, anchors.length);
var j=loopend - begin;
+
var j = loopend - begin;
log ('setupTooltips: anchors.length=' + anchors.length + ', begin=' + begin +
+
log(
', howmany=' + howmany + ', loopend=' + loopend + ', remove=' + remove);
+
'setupTooltips: anchors.length=' +
var doTooltip= remove ? removeTooltip : addTooltip;
+
anchors.length +
// try a faster (?) loop construct
+
', begin=' +
if (j > 0) {
+
begin +
do {
+
', howmany=' +
var a=anchors[loopend - j];
+
howmany +
if (typeof a==='undefined' || !a || !a.href) {
+
', loopend=' +
log('got null anchor at index ' + loopend - j);
+
loopend +
continue;
+
', remove=' +
 +
remove
 +
);
 +
var doTooltip = remove ? removeTooltip : addTooltip;
 +
// try a faster (?) loop construct
 +
if (j > 0) {
 +
do {
 +
var 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(function () {
 +
setupTooltipsLoop(anchors, finish, howmany, sleep, remove, popData);
 +
}, sleep);
 +
} else {
 +
if (!remove && !getValueOf('popupTocLinks')) {
 +
rmTocTooltips();
 
}
 
}
doTooltip(a, popData);
+
pg.flag.finishedLoading = true;
} while (--j);
+
}
 
}
 
}
if (finish < anchors.length) {
+
 
setTimeout(function() {
+
// eliminate popups from the TOC
setupTooltipsLoop(anchors,finish,howmany,sleep,remove,popData);},
+
// This also kills any onclick stuff that used to be going on in the toc
sleep);
+
function rmTocTooltips() {
} else {
+
var toc = document.getElementById('toc');
if ( !remove && ! getValueOf('popupTocLinks')) { rmTocTooltips(); }
+
if (toc) {
pg.flag.finishedLoading=true;
+
var tocLinks = toc.getElementsByTagName('A');
 +
var tocLen = tocLinks.length;
 +
for (var j = 0; j < tocLen; ++j) {
 +
removeTooltip(tocLinks[j], true);
 +
}
 +
}
 
}
 
}
}
 
  
// eliminate popups from the TOC
+
function addTooltip(a, popData) {
// This also kills any onclick stuff that used to be going on in the toc
+
if (!isPopupLink(a)) {
function rmTocTooltips() {
+
return;
var toc=document.getElementById('toc');
 
if (toc) {
 
var tocLinks=toc.getElementsByTagName('A');
 
var tocLen = tocLinks.length;
 
for (j=0; j<tocLen; ++j) {
 
removeTooltip(tocLinks[j], true);
 
 
}
 
}
 +
a.onmouseover = mouseOverWikiLink;
 +
a.onmouseout = mouseOutWikiLink;
 +
a.onmousedown = killPopup;
 +
a.hasPopup = true;
 +
a.popData = popData;
 
}
 
}
}
 
  
function addTooltip(a, popData) {
+
function removeTooltip(a) {
if ( !isPopupLink(a) ) { return; }
+
if (!a.hasPopup) {
a.onmouseover=mouseOverWikiLink;
+
return;
a.onmouseout= mouseOutWikiLink;
+
}
a.onmousedown = killPopup;
+
a.onmouseover = null;
a.hasPopup = true;
+
a.onmouseout = null;
a.popData = popData;
+
if (a.originalTitle) {
}
+
a.title = a.originalTitle;
 
+
}
function removeTooltip(a) {
+
a.hasPopup = false;
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) {
+
function removeTitle(a) {
if ( a.title || !a.originalTitle ) { return; }
+
if (!a.originalTitle) {
a.title = a.originalTitle;
+
a.originalTitle = a.title;
}
+
}
 +
a.title = '';
 +
}
  
function registerHooks(np) {
+
function restoreTitle(a) {
var popupMaxWidth=getValueOf('popupMaxWidth');
+
if (a.title || !a.originalTitle) {
 +
return;
 +
}
 +
a.title = a.originalTitle;
 +
}
  
if (typeof popupMaxWidth === 'number') {
+
function registerHooks(np) {
var setMaxWidth = function () {
+
var popupMaxWidth = getValueOf('popupMaxWidth');
np.mainDiv.style.maxWidth = popupMaxWidth + 'px';
 
np.maxWidth = popupMaxWidth;
 
  
try {
+
if (typeof popupMaxWidth === 'number') {
// hack for IE
+
var setMaxWidth = function () {
// see http://www.svendtofte.com/code/max_width_in_ie/
+
np.mainDiv.style.maxWidth = popupMaxWidth + 'px';
// use setExpression as documented here on msdn: http://tinyurl dot com/dqljn
+
np.maxWidth = popupMaxWidth;
+
};
if (np.mainDiv.style.setExpression) {
+
np.addHook(setMaxWidth, 'unhide', 'before');
np.mainDiv.style.setExpression(
+
}
'width', 'document.body.clientWidth > ' +
+
//<NOLITE>
popupMaxWidth + ' ? "' +popupMaxWidth + 'px": "auto"');
+
np.addHook(addPopupShortcuts, 'unhide', 'after');
}
+
np.addHook(rmPopupShortcuts, 'hide', 'before');
}
+
//</NOLITE>
catch (errors) {
 
errlog( "Running on IE8 are we not?: " + errors );
 
}
 
};
 
np.addHook(setMaxWidth, 'unhide', 'before');
 
 
}
 
}
//<NOLITE>
 
np.addHook(addPopupShortcuts, 'unhide', 'after');
 
np.addHook(rmPopupShortcuts, 'hide', 'before');
 
//</NOLITE>
 
}
 
  
 +
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) {
+
function mouseOverWikiLink(evt) {
if (!evt && window.event) {evt=window.event;}
+
if (!evt && window.event) {
return mouseOverWikiLink2(this, evt);
+
evt = window.event;
}
+
}
  
/**
+
// if the modifier is needed, listen for it,
* Gets the references list item that the provided footnote link targets. This
+
// we will remove the listener when we mouseout of this link or kill popup.
* is typically a li element within the ol.references element inside the reflist.
+
if (getValueOf('popupModifier')) {
* @param {Element} a - A footnote link.
+
// if popupModifierAction = enable, we should popup when the modifier is pressed
* @returns {Element|boolean} The targeted element, or false if one can't be found.
+
// if popupModifierAction = disable, we should popup unless the modifier is pressed
*/
+
var action = getValueOf('popupModifierAction');
function footnoteTarget(a) {
+
var key = action == 'disable' ? 'keyup' : 'keydown';
var aTitle=Title.fromAnchor(a);
+
var a = this;
// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly
+
a.modifierKeyHandler = function (evt) {
var anch = aTitle.anchor;
+
mouseOverWikiLink2(a, evt);
if ( ! /^(cite_note-|_note-|endnote)/.test(anch) ) { return false; }
+
};
 
+
document.addEventListener(key, a.modifierKeyHandler, false);
var lTitle=Title.fromURL(location.href);
+
}
if ( lTitle.toString(true) !== aTitle.toString(true) ) { return false; }
 
  
var el=document.getElementById(anch);
+
return mouseOverWikiLink2(this, evt);
while ( el && typeof el.nodeName === 'string') {
 
var 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('<hr />' + x.innerHTML, 'popupPreview', navpop.idNumber, getValueOf('popupSubpopups'));
+
* 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.
 +
* @returns {Element|boolean} The targeted element, or false if one can't be found.
 +
*/
 +
function footnoteTarget(a) {
 +
var aTitle = Title.fromAnchor(a);
 +
// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly
 +
var anch = aTitle.anchor;
 +
if (!/^(cite_note-|_note-|endnote)/.test(anch)) {
 +
return false;
 +
}
  
// var modid=0;
+
var lTitle = Title.fromURL(location.href);
// if(!window.opera) { window.opera={postError: console.log}; }
+
if (lTitle.toString(true) !== aTitle.toString(true)) {
 +
return false;
 +
}
  
function modifierKeyHandler(a) {
+
var el = document.getElementById(anch);
return function(evt) {
+
while (el && typeof el.nodeName === 'string') {
// opera.postError('modifierKeyHandler called' + (++modid));
+
var nt = el.nodeName.toLowerCase();
// opera.postError(''+evt + modid);
+
if (nt === 'li') {
// for (var i in evt) {
+
return el;
// opera.postError('' + modid + ' ' + i + ' ' + evt[i]);
+
} else if (nt === 'body') {
// }
+
return false;
// opera.postError(''+evt.ctrlKey + modid);
+
} else if (el.parentNode) {
var mod=getValueOf('popupModifier');
+
el = el.parentNode;
if (!mod) { return true; }
+
} else {
 +
return false;
 +
}
 +
}
 +
return false;
 +
}
  
if (!evt && window.event) {evt=window.event;}
+
function footnotePreview(x, navpop) {
// opera.postError('And now....'+modid);
+
setPopupHTML('<hr />' + x.innerHTML, 'popupPreview', navpop.idNumber);
// opera.postError(''+evt+modid);
+
}
// opera.postError(''+evt.ctrlKey+modid);
 
  
var modPressed = modifierPressed(evt);
+
function modifierPressed(evt) {
var action = getValueOf('popupModifierAction');
+
var mod = getValueOf('popupModifier');
 +
if (!mod) {
 +
return false;
 +
}
  
// FIXME: probable bug - modifierPressed should be modPressed below?
+
if (!evt && window.event) {
if ( action === 'disable' && modifierPressed ) { return true; }
+
evt = window.event;
if ( action === 'enable' && !modifierPressed ) { return true; }
+
}
  
mouseOverWikiLink2(a, evt);
+
return evt && mod && evt[mod.toLowerCase() + 'Key'];
};
+
}
}
 
  
function modifierPressed(evt) {
+
// Checks if the correct modifier pressed/unpressed if needed
var mod=getValueOf('popupModifier');
+
function isCorrectModifier(a, evt) {
if (!mod) { return false; }
+
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
 +
var action = getValueOf('popupModifierAction');
 +
return (
 +
(action == 'enable' && modifierPressed(evt)) || (action == 'disable' && !modifierPressed(evt))
 +
);
 +
}
  
if (!evt && window.event) {evt=window.event;}
+
function mouseOverWikiLink2(a, evt) {
// opera.postError('And now....'+modid);
+
if (!isCorrectModifier(a, evt)) {
// opera.postError(''+evt+modid);
+
return;
// opera.postError(''+evt.ctrlKey+modid);
+
}
 +
if (getValueOf('removeTitles')) {
 +
removeTitle(a);
 +
}
 +
if (a == pg.current.link && a.navpopup && a.navpopup.isVisible()) {
 +
return;
 +
}
 +
pg.current.link = a;
  
return ( evt && mod && evt[mod.toLowerCase() + 'Key'] );
+
if (getValueOf('simplePopups') && !pg.option.popupStructure) {
 +
// reset *default value* of popupStructure
 +
setDefault('popupStructure', 'original');
 +
}
  
}
+
var article = new Title().fromAnchor(a);
 +
// set global variable (ugh) to hold article (wikipage)
 +
pg.current.article = article;
  
function dealWithModifier(a,evt) {
+
if (!a.navpopup) {
if (!getValueOf('popupModifier')) { return false; }
+
a.navpopup = newNavpopup(a, article);
var action = getValueOf('popupModifierAction');
+
pg.current.linksHash[a.href] = a.navpopup;
if ( action ==  'enable' && !modifierPressed(evt) ||
+
pg.current.links.push(a);
    action == 'disable' &&  modifierPressed(evt) ) {
 
// if the modifier is needed and not pressed, listen for it until
 
// we mouseout of this link.
 
restoreTitle(a);
 
var addHandler='addEventListener';
 
var rmHandler='removeEventListener';
 
var on='';
 
if (!document.addEventListener) {
 
addHandler='attachEvent';
 
rmHandler='detachEvent';
 
on='on';
 
 
}
 
}
if (!document[addHandler]) { // forget it
+
if (a.navpopup.pending === null || a.navpopup.pending !== 0) {
return;
+
// either fresh popups or those with unfinshed business are redone from scratch
 +
simplePopupContent(a, article);
 
}
 
}
 +
a.navpopup.showSoonIfStable(a.navpopup.delay);
  
a.modifierKeyHandler=modifierKeyHandler(a);
+
clearInterval(pg.timer.checkPopupPosition);
 +
pg.timer.checkPopupPosition = setInterval(checkPopupPosition, 600);
  
switch (action) {
+
if (getValueOf('simplePopups')) {
case 'enable':
+
if (getValueOf('popupPreviewButton') && !a.simpleNoMore) {
document[addHandler](on+'keydown', a.modifierKeyHandler, false);
+
var d = document.createElement('div');
a[addHandler](on+'mouseout', function() {
+
d.className = 'popupPreviewButtonDiv';
document[rmHandler](on+'keydown',
+
var s = document.createElement('span');
a.modifierKeyHandler, false);
+
d.appendChild(s);
}, true);
+
s.className = 'popupPreviewButton';
break;
+
s['on' + getValueOf('popupPreviewButtonEvent')] = function () {
case 'disable':
+
a.simpleNoMore = true;
document[addHandler](on+'keyup', a.modifierKeyHandler, false);
+
d.style.display = 'none';
 +
nonsimplePopupContent(a, article);
 +
};
 +
s.innerHTML = popupString('show preview');
 +
setPopupHTML(d, 'popupPreview', a.navpopup.idNumber);
 +
}
 
}
 
}
  
return true;
+
if (a.navpopup.pending !== 0) {
 +
nonsimplePopupContent(a, article);
 +
}
 
}
 
}
return false;
 
}
 
  
function mouseOverWikiLink2(a, evt) {
+
// simplePopupContent: the content that do not require additional download
if (dealWithModifier(a,evt)) { return; }
+
// (it is shown even when simplePopups is true)
if ( getValueOf('removeTitles') ) { removeTitle(a); }
+
function simplePopupContent(a, article) {
if ( a==pg.current.link && a.navpopup && a.navpopup.isVisible() ) { return; }
+
/* FIXME hack */ a.navpopup.hasPopupMenu = false;
pg.current.link=a;
+
a.navpopup.setInnerHTML(popupHTML(a));
 +
fillEmptySpans({ navpopup: a.navpopup });
  
if (getValueOf('simplePopups') && pg.option.popupStructure === null) {
+
if (getValueOf('popupDraggable')) {
// reset *default value* of popupStructure
+
var dragHandle = getValueOf('popupDragHandle') || null;
setDefault('popupStructure', 'original');
+
if (dragHandle && dragHandle != 'all') {
 +
dragHandle += a.navpopup.idNumber;
 +
}
 +
setTimeout(function () {
 +
a.navpopup.makeDraggable(dragHandle);
 +
}, 150);
 +
}
 +
 
 +
//<NOLITE>
 +
if (getValueOf('popupRedlinkRemoval') && a.className == 'new') {
 +
setPopupHTML('<br>' + popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);
 +
}
 +
//</NOLITE>
 
}
 
}
  
var article=(new Title()).fromAnchor(a);
+
function debugData(navpopup) {
// set global variable (ugh) to hold article (wikipage)
+
if (getValueOf('popupDebugging') && navpopup.idNumber) {
pg.current.article = article;
+
setPopupHTML(
 
+
'idNumber=' + navpopup.idNumber + ', pending=' + navpopup.pending,
if (!a.navpopup) {
+
'popupError',
// FIXME: this doesn't behave well if you mouse out of a popup
+
navpopup.idNumber
// directly into a link with the same href
+
);
if (pg.current.linksHash[a.href] && false) {
 
a.navpopup = pg.current.linksHash[a.href];
 
}
 
else {
 
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
+
function newNavpopup(a, article) {
simplePopupContent(a, article);
+
var 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;
 
}
 
}
a.navpopup.showSoonIfStable(a.navpopup.delay);
 
  
getValueOf('popupInitialWidth');
+
// 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;
 +
}
  
clearInterval(pg.timer.checkPopupPosition);
+
// Should we show nonsimple context govern by the option (e.g. popupUserInfo)?
pg.timer.checkPopupPosition=setInterval(checkPopupPosition, 600);
+
// 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];
 +
}
 +
}
  
if(getValueOf('simplePopups')) {
+
function nonsimplePopupContent(a, article) {
if (getValueOf('popupPreviewButton') && !a.simpleNoMore) {
+
var diff = null,
var d=document.createElement('div');
+
history = null;
d.className='popupPreviewButtonDiv';
+
var params = parseParams(a.href);
var s=document.createElement('span');
+
var oldid = typeof params.oldid == 'undefined' ? null : params.oldid;
d.appendChild(s);
+
//<NOLITE>
s.className='popupPreviewButton';
+
if (shouldShow(a, 'popupPreviewDiffs')) {
s['on' + getValueOf('popupPreviewButtonEvent')] = function() {
+
diff = params.diff;
a.simpleNoMore=true;
+
}
nonsimplePopupContent(a,article);
+
if (shouldShow(a, 'popupPreviewHistory')) {
};
+
history = params.action == 'history';
s.innerHTML=popupString('show preview');
+
}
setPopupHTML(d, 'popupPreview', a.navpopup.idNumber);
+
//</NOLITE>
 +
a.navpopup.pending = 0;
 +
var referenceElement = footnoteTarget(a);
 +
if (referenceElement) {
 +
footnotePreview(referenceElement, a.navpopup);
 +
//<NOLITE>
 +
} 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);
 +
//</NOLITE>
 +
} 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);
 
}
 
}
return;
 
 
}
 
}
  
if (a.navpopup.pending !== 0 ) {
+
function pendingNavpopTask(navpop) {
nonsimplePopupContent(a, article);
+
if (navpop && navpop.pending === null) {
 +
navpop.pending = 0;
 +
}
 +
++navpop.pending;
 +
debugData(navpop);
 
}
 
}
}
 
  
// simplePopupContent: the content that is shown even when simplePopups is true
+
function completedNavpopTask(navpop) {
function simplePopupContent(a, article) {
+
if (navpop && navpop.pending) {
/* FIXME hack */ a.navpopup.hasPopupMenu=false;
+
--navpop.pending;
a.navpopup.setInnerHTML(popupHTML(a));
 
fillEmptySpans({navpopup:a.navpopup});
 
 
 
if (getValueOf('popupDraggable'))
 
{
 
var dragHandle = getValueOf('popupDragHandle') || null;
 
if (dragHandle && dragHandle != 'all') {
 
dragHandle += a.navpopup.idNumber;
 
 
}
 
}
setTimeout(function(){a.navpopup.makeDraggable(dragHandle);}, 150);
+
debugData(navpop);
 
}
 
}
  
//<NOLITE>
+
function startArticlePreview(article, oldid, navpop) {
if (getValueOf('popupRedlinkRemoval') && a.className=='new') {
+
navpop.redir = 0;
setPopupHTML('<br>'+popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);
+
loadPreview(article, oldid, navpop);
 
}
 
}
//</NOLITE>
 
}
 
  
function debugData(navpopup) {
+
function loadPreview(article, oldid, navpop) {
if(getValueOf('popupDebugging') && navpopup.idNumber) {
+
if (!navpop.redir) {
setPopupHTML('idNumber='+navpopup.idNumber + ', pending=' + navpopup.pending,
+
navpop.originalArticle = article;
'popupError', navpopup.idNumber);
+
}
 +
article.oldid = oldid;
 +
loadAPIPreview('revision', article, navpop);
 
}
 
}
}
 
 
function newNavpopup(a, article) {
 
var 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;
 
}
 
 
  
function nonsimplePopupContent(a, article) {
+
function loadPreviewFromRedir(redirMatch, navpop) {
var diff=null, history=null;
+
// redirMatch is a regex match
var params=parseParams(a.href);
+
var target = new Title().fromWikiText(redirMatch[2]);
var oldid=(typeof params.oldid=='undefined' ? null : params.oldid);
+
// overwrite (or add) anchor from original target
//<NOLITE>
+
// mediawiki does overwrite; eg [[User:Lupin/foo3#Done]]
if(getValueOf('popupPreviewDiffs')) {
+
if (navpop.article.anchor) {
diff=params.diff;
+
target.anchor = navpop.article.anchor;
}
 
if(getValueOf('popupPreviewHistory')) {
 
history=(params.action=='history');
 
}
 
//</NOLITE>
 
a.navpopup.pending=0;
 
var referenceElement = footnoteTarget(a);
 
if (referenceElement) {
 
footnotePreview(referenceElement, a.navpopup);
 
//<NOLITE>
 
} else if ( diff || diff === 0 ) {
 
loadDiff(article, oldid, diff, a.navpopup);
 
} else if ( history ) {
 
loadAPIPreview('history', article, a.navpopup);
 
} else if ( pg.re.contribs.test(a.href) ) {
 
loadAPIPreview('contribs', article, a.navpopup);
 
} else if ( 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 &&
 
( getValueOf('imagePopupsForImages') || ! anchorContainsImage(a) )
 
) {
 
loadAPIPreview('imagepagepreview', article, a.navpopup);
 
loadImage(article, a.navpopup);
 
//</NOLITE>
 
} else {
 
if (article.namespaceId() == pg.nsCategoryId &&
 
getValueOf('popupCategoryMembers')) {
 
loadAPIPreview('category', article, a.navpopup);
 
} else if ((article.namespaceId() == pg.nsUserId || article.namespaceId() == pg.nsUsertalkId) &&
 
getValueOf('popupUserInfo')) {
 
loadAPIPreview('userinfo', article, a.navpopup);
 
 
}
 
}
startArticlePreview(article, oldid, a.navpopup);
+
navpop.redir++;
 +
navpop.redirTarget = target;
 +
//<NOLITE>
 +
var warnRedir = redirLink(target, navpop.article);
 +
setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);
 +
//</NOLITE>
 +
navpop.article = target;
 +
fillEmptySpans({ redir: true, redirTarget: target, navpopup: navpop });
 +
return loadPreview(target, null, navpop);
 
}
 
}
}
 
  
function pendingNavpopTask(navpop) {
+
function insertPreview(download) {
if (navpop && navpop.pending === null) { navpop.pending=0; }
+
if (!download.owner) {
++navpop.pending;
+
return;
debugData(navpop);
+
}
}
 
  
function completedNavpopTask(navpop) {
+
var redirMatch = pg.re.redirect.exec(download.data);
if (navpop && navpop.pending) { --navpop.pending; }
+
if (download.owner.redir === 0 && redirMatch) {
debugData(navpop);
+
loadPreviewFromRedir(redirMatch, download.owner);
}
+
return;
 +
}
  
function startArticlePreview(article, oldid, navpop) {
+
if (download.owner.visible || !getValueOf('popupLazyPreviews')) {
navpop.redir=0;
+
insertPreviewNow(download);
loadPreview(article, oldid, navpop);
+
} else {
}
+
var id = download.owner.redir ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';
 
+
download.owner.addHook(
function loadPreview(article, oldid, navpop) {
+
function () {
pendingNavpopTask(navpop);
+
insertPreviewNow(download);
if (!navpop.redir) { navpop.originalArticle=article; }
+
return true;
if (!navpop.visible && getValueOf('popupLazyDownloads')) {
+
},
var id=(navpop.redir) ? 'DOWNLOAD_PREVIEW_REDIR_HOOK' : 'DOWNLOAD_PREVIEW_HOOK';
+
'unhide',
navpop.addHook(function() {
+
'after',
getWiki(article, insertPreview, oldid, navpop);
+
id
return true; }, 'unhide', 'before', id);
+
);
} else {
+
}
getWiki(article, insertPreview, oldid, navpop);
 
 
}
 
}
}
 
  
function loadPreviewFromRedir(redirMatch, navpop) {
+
function insertPreviewNow(download) {
// redirMatch is a regex match
+
if (!download.owner) {
var target = new Title().fromWikiText(redirMatch[2]);
+
return;
// overwrite (or add) anchor from original target
+
}
// mediawiki does overwrite; eg [[User:Lupin/foo3#Done]]
+
var wikiText = download.data;
if ( navpop.article.anchor ) { target.anchor = navpop.article.anchor; }
+
var navpop = download.owner;
var trailingRubbish=redirMatch[4];
+
var art = navpop.redirTarget || navpop.originalArticle;
navpop.redir++;
 
navpop.redirTarget=target;
 
//<NOLITE>
 
var warnRedir = redirLink(target, navpop.article);
 
setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);
 
//</NOLITE>
 
navpop.article=target;
 
fillEmptySpans({redir: true, redirTarget: target, navpopup:navpop});
 
return loadPreview(target, null,  navpop);
 
}
 
  
function insertPreview(download) {
+
//<NOLITE>
if (!download.owner) { return; }
+
makeFixDabs(wikiText, navpop);
 +
if (getValueOf('popupSummaryData')) {
 +
getPageInfo(wikiText, download);
 +
setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);
 +
}
 +
 
 +
var imagePage = '';
 +
if (art.namespaceId() == pg.nsImageId) {
 +
imagePage = art.toString();
 +
} else {
 +
imagePage = getValidImageFromWikiText(wikiText);
 +
}
 +
if (imagePage) {
 +
loadImage(Title.fromWikiText(imagePage), navpop);
 +
}
 +
//</NOLITE>
  
var redirMatch = pg.re.redirect.exec(download.data);
+
if (getValueOf('popupPreviews')) {
if (download.owner.redir === 0 && redirMatch) {
+
insertArticlePreview(download, art, navpop);
completedNavpopTask(download.owner);
+
}
loadPreviewFromRedir(redirMatch, download.owner);
 
return;
 
 
}
 
}
  
if (download.owner.visible || !getValueOf('popupLazyPreviews')) {
+
function insertArticlePreview(download, art, navpop) {
insertPreviewNow(download);
+
if (download && typeof download.data == typeof '') {
} else {
+
if (art.namespaceId() == pg.nsTemplateId && getValueOf('popupPreviewRawTemplates')) {
var id=(download.owner.redir) ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';
+
// FIXME compare/consolidate with diff escaping code for wikitext
download.owner.addHook( function(){insertPreviewNow(download); return true;},
+
var h =
'unhide', 'after', id );
+
'<hr /><span style="font-family: monospace;">' +
 +
download.data.entify().split('\\n').join('<br />\\n') +
 +
'</span>';
 +
setPopupHTML(h, 'popupPreview', navpop.idNumber);
 +
} else {
 +
var p = prepPreviewmaker(download.data, art, navpop);
 +
p.showPreview();
 +
}
 +
}
 
}
 
}
}
 
  
function insertPreviewNow(download) {
+
function prepPreviewmaker(data, article, navpop) {
if (!download.owner) { return; }
+
// deal with tricksy anchors
var wikiText=download.data;
+
var d = anchorize(data, article.anchorString());
var navpop=download.owner;
+
var urlBase = joinPath([pg.wiki.articlebase, article.urlString()]);
completedNavpopTask(navpop);
+
var p = new Previewmaker(d, urlBase, navpop);
var art=navpop.redirTarget || navpop.originalArticle;
+
return p;
 
 
//<NOLITE>
 
makeFixDabs(wikiText, navpop);
 
if (getValueOf('popupSummaryData')) {
 
var info=getPageInfo(wikiText, download);
 
setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);
 
 
}
 
}
  
var imagePage='';
+
// Try to imitate the way mediawiki generates HTML anchors from section titles
if (art.namespaceId()==pg.nsImageId) { imagePage=art.toString(); }
+
function anchorize(d, anch) {
else { imagePage=getValidImageFromWikiText(wikiText); }
+
if (!anch) {
if(imagePage) { loadImage(Title.fromWikiText(imagePage), navpop); }
+
return d;
//</NOLITE>
+
}
 +
var anchRe = RegExp(
 +
'(?:=+\\s*' +
 +
literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') +
 +
'\\s*=+|\\{\\{\\s*' +
 +
getValueOf('popupAnchorRegexp') +
 +
'\\s*(?:\\|[^|}]*)*?\\s*' +
 +
literalizeRegex(anch) +
 +
'\\s*(?:\\|[^}]*)?}})'
 +
);
 +
var match = d.match(anchRe);
 +
if (match && match.length > 0 && match[0]) {
 +
return d.substring(d.indexOf(match[0]));
 +
}
  
if (getValueOf('popupPreviews')) { insertArticlePreview(download, art, navpop); }
+
// now try to deal with == foo [[bar|baz]] boom == -> #foo_baz_boom
 +
var lines = d.split('\n');
 +
for (var i = 0; i < lines.length; ++i) {
 +
lines[i] = lines[i]
 +
.replace(RegExp('[[]{2}([^|\\]]*?[|])?(.*?)[\\]]{2}', 'g'), '$2')
 +
.replace(/'''([^'])/g, '$1')
 +
.replace(RegExp("''([^'])", 'g'), '$1');
 +
if (lines[i].match(anchRe)) {
 +
return d.split('\n').slice(i).join('\n').replace(RegExp('^[^=]*'), '');
 +
}
 +
}
 +
return d;
 +
}
  
}
+
function killPopup() {
 
+
removeModifierKeyHandler(this);
function insertArticlePreview(download, art, navpop) {
+
if (getValueOf('popupShortcutKeys')) {
if (download && typeof download.data == typeof ''){
+
rmPopupShortcuts();
if (art.namespaceId()==pg.nsTemplateId && getValueOf('popupPreviewRawTemplates')) {
+
}
// FIXME compare/consolidate with diff escaping code for wikitext
+
if (!pg) {
var h='<hr /><span style="font-family: monospace;">' + download.data.entify().split('\\n').join('<br />\\n') + '</span>';
+
return;
setPopupHTML(h, 'popupPreview', navpop.idNumber);
+
}
 +
if (pg.current.link && pg.current.link.navpopup) {
 +
pg.current.link.navpopup.banish();
 
}
 
}
else {
+
pg.current.link = null;
var p=prepPreviewmaker(download.data, art, navpop);
+
abortAllDownloads();
p.showPreview();
+
if (pg.timer.checkPopupPosition) {
 +
clearInterval(pg.timer.checkPopupPosition);
 +
pg.timer.checkPopupPosition = null;
 
}
 
}
 +
return true; // preserve default action
 
}
 
}
}
+
// ENDFILE: actions.js
  
function prepPreviewmaker(data, article, navpop) {
+
// STARTFILE: domdrag.js
// deal with tricksy anchors
+
/**
var d=anchorize(data, article.anchorString());
+
@fileoverview
var urlBase=joinPath([pg.wiki.articlebase, article.urlString()]);
+
The {@link Drag} object, which enables objects to be dragged around.
var p=new Previewmaker(d, urlBase, navpop);
 
return p;
 
}
 
  
 +
<pre>
 +
*************************************************
 +
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]]
  
// Try to imitate the way mediawiki generates HTML anchors from section titles
+
Copyright Aaron Boodman.
function anchorize(d, anch) {
+
Saying stupid things daily since March 2001.
if (!anch) { return d; }
+
</pre>
var anchRe=RegExp('(?:=+\\s*' + literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') + '\\s*=+|\\{\\{\\s*'+getValueOf('popupAnchorRegexp')+'\\s*(?:\\|[^|}]*)*?\\s*'+literalizeRegex(anch)+'\\s*(?:\\|[^}]*)?\}\})');
+
*/
var match=d.match(anchRe);
 
if(match && match.length > 0 && match[0]) { return d.substring(d.indexOf(match[0])); }
 
  
// now try to deal with == foo [[bar|baz]] boom == -> #foo_baz_boom
+
/**
var lines=d.split('\n');
+
* Creates a new Drag object. This is used to make various DOM elements draggable.
for (var i=0; i<lines.length; ++i) {
+
* @constructor
lines[i]=lines[i].replace(RegExp('[[]{2}([^|\\]]*?[|])?(.*?)[\\]]{2}', 'g'), '$2')
+
*/
.replace(/'''([^'])/g, '$1').replace(RegExp("''([^'])", 'g'), '$1');
+
function Drag() {
if (lines[i].match(anchRe)) {
+
/**
return d.split('\n').slice(i).join('\n').replace(RegExp('^[^=]*'), '');
+
* Condition to determine whether or not to drag. This function should take one parameter,
}
+
* an Event. To disable this, set it to <code>null</code>.
}
+
* @type Function
return d;
+
*/
}
+
this.startCondition = null;
  
function killPopup() {
+
/**
if (getValueOf('popupShortcutKeys')) { rmPopupShortcuts(); }
+
* Hook to be run when the drag finishes. This is passed the final coordinates of the
if (!pg) { return; }
+
* dragged object (two integers, x and y). To disables this, set it to <code>null</code>.
if (pg.current.link && pg.current.link.navpopup) { pg.current.link.navpopup.banish(); }
+
* @type Function
pg.current.link=null;
+
*/
abortAllDownloads();
+
this.endHook = null;
if (pg.timer.checkPopupPosition) {
 
clearInterval(pg.timer.checkPopupPosition);
 
pg.timer.checkPopupPosition=null;
 
 
}
 
}
return true; // preserve default action
 
}
 
// ENDFILE: actions.js
 
// STARTFILE: domdrag.js
 
/**
 
  @fileoverview
 
  The {@link Drag} object, which enables objects to be dragged around.
 
  
  <pre>
 
  *************************************************
 
  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.
 
  </pre>
 
*/
 
 
/**
 
  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.
+
* Gets an event in a cross-browser manner.
  To disable this, set it to <code>null</code>.
+
* @param {Event} e
  @type Function
+
* @private
*/
+
*/
this.startCondition = null;
+
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;
 +
};
 +
 
 
/**
 
/**
  Hook to be run when the drag finishes. This is passed the final coordinates of
+
* Initialises the Drag instance by telling it which object you want to be draggable, and what
  the dragged object (two integers, x and y). To disables this, set it to <code>null</code>.
+
* you want to drag it by.
  @type Function
+
* @param {DOMElement} o The "handle" by which <code>oRoot</code> is dragged.
*/
+
* @param {DOMElement} oRoot The object which moves when <code>o</code> is dragged, or <code>o</code> if omitted.
this.endHook = null;
+
*/
}
+
Drag.prototype.init = function (o, oRoot) {
 +
var dragObj = this;
 +
this.obj = o;
 +
o.onmousedown = function (e) {
 +
dragObj.start.apply(dragObj, [e]);
 +
};
 +
o.dragging = false;
 +
o.popups_draggable = true;
 +
o.hmode = true;
 +
o.vmode = true;
  
/**
+
o.root = oRoot ? oRoot : o;
  Gets an event in a cross-browser manner.
 
  @param {Event} e
 
  @private
 
*/
 
Drag.prototype.fixE = function(e) {
 
if (typeof e == 'undefined') { e = window.event; }
 
if (typeof e.layerX == 'undefined') { e.layerX = e.offsetX; }
 
if (typeof e.layerY == 'undefined') { e.layerY = e.offsetY; }
 
return e;
 
};
 
/**
 
  Initialises the Drag instance by telling it which object you want to be draggable, and what you want to drag it by.
 
  @param {DOMElement} o The "handle" by which <code>oRoot</code> is dragged.
 
  @param {DOMElement} oRoot The object which moves when <code>o</code> is dragged, or <code>o</code> if omitted.
 
*/
 
Drag.prototype.init = function(o, oRoot) {
 
var dragObj   = this;
 
this.obj = o;
 
o.onmousedown = function(e) { dragObj.start.apply( dragObj, [e]); };
 
o.dragging   = false;
 
o.popups_draggable   = true;
 
o.hmode   = true;
 
o.vmode   = true;
 
  
o.root = oRoot ? oRoot : o ;
+
if (isNaN(parseInt(o.root.style.left, 10))) {
 +
o.root.style.left = '0px';
 +
}
 +
if (isNaN(parseInt(o.root.style.top, 10))) {
 +
o.root.style.top = '0px';
 +
}
  
if (isNaN(parseInt(o.root.style.left, 10))) { o.root.style.left  = "0px"; }
+
o.root.onthisStart = function () {};
if (isNaN(parseInt(o.root.style.top,  10))) { o.root.style.top = "0px"; }
+
o.root.onthisEnd = function () {};
 +
o.root.onthis = function () {};
 +
};
  
o.root.onthisStart  = function(){};
+
/**
o.root.onthisEnd = function(){};
+
* Starts the drag.
o.root.onthis   = function(){};
+
* @private
};
+
* @param {Event} e
 +
*/
 +
Drag.prototype.start = function (e) {
 +
var o = this.obj; // = this;
 +
e = this.fixE(e);
 +
if (this.startCondition && !this.startCondition(e)) {
 +
return;
 +
}
 +
var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
 +
var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
 +
o.root.onthisStart(x, y);
  
/**
+
o.lastMouseX = e.clientX;
  Starts the drag.
+
o.lastMouseY = e.clientY;
  @private
 
  @param {Event} e
 
*/
 
Drag.prototype.start = function(e) {
 
var o = this.obj; // = this;
 
e = this.fixE(e);
 
if (this.startCondition && !this.startCondition(e)) { return; }
 
var y = parseInt(o.vmode ? o.root.style.top  : o.root.style.bottom, 10);
 
var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right,  10);
 
o.root.onthisStart(x, y);
 
  
o.lastMouseX = e.clientX;
+
var dragObj = this;
o.lastMouseY = e.clientY;
+
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;
 +
};
  
var dragObj   = this;
+
/**
o.onmousemoveDefault = document.onmousemove;
+
* Does the drag.
o.dragging   = true;
+
* @param {Event} e
document.onmousemove = function(e) { dragObj.drag.apply( dragObj, [e] ); };
+
* @private
document.onmouseup   = function(e) { dragObj.end.apply( dragObj, [e] ); };
+
*/
return false;
+
Drag.prototype.drag = function (e) {
};
+
e = this.fixE(e);
/**
+
var o = this.obj;
  Does the drag.
 
  @param {Event} e
 
  @private
 
*/
 
Drag.prototype.drag = function(e) {
 
e = this.fixE(e);
 
var o = this.obj;
 
  
var ey = e.clientY;
+
var ey = e.clientY;
var ex = e.clientX;
+
var ex = e.clientX;
var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
+
var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10 );
+
var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
var nx, ny;
+
var nx, ny;
  
nx = x + ((ex - o.lastMouseX) * (o.hmode ? 1 : -1));
+
nx = x + (ex - o.lastMouseX) * (o.hmode ? 1 : -1);
ny = y + ((ey - o.lastMouseY) * (o.vmode ? 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.hmode ? 'left' : 'right'] = nx + 'px';
this.obj.root.style[o.vmode ? "top" : "bottom"] = ny + "px";
+
this.obj.root.style[o.vmode ? 'top' : 'bottom'] = ny + 'px';
this.obj.lastMouseX = ex;
+
this.obj.lastMouseX = ex;
this.obj.lastMouseY = ey;
+
this.obj.lastMouseY = ey;
  
this.obj.root.onthis(nx, ny);
+
this.obj.root.onthis(nx, ny);
return false;
+
return false;
};
+
};
  
/**
+
/**
  Ends the drag.
+
* Ends the drag.
  @private
+
* @private
*/
+
*/
Drag.prototype.end = function() {
+
Drag.prototype.end = function () {
document.onmousemove=this.obj.onmousemoveDefault;
+
document.onmousemove = this.obj.onmousemoveDefault;
document.onmouseup   = null;
+
document.onmouseup = null;
this.obj.dragging = false;
+
this.obj.dragging = false;
if (this.endHook) {
+
if (this.endHook) {
this.endHook( parseInt(this.obj.root.style[this.obj.hmode ? "left" : "right"], 10),
+
this.endHook(
  parseInt(this.obj.root.style[this.obj.vmode ? "top" : "bottom"], 10));
+
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
+
};
//<NOLITE>
+
// ENDFILE: domdrag.js
pg.structures.original={};
 
pg.structures.original.popupLayout=function () {
 
return ['popupError', 'popupImage', 'popupTopLinks', 'popupTitle',
 
'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('<b><<mainlink>></b>',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;
 
  
 +
// STARTFILE: structures.js
 +
//<NOLITE>
 +
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('<b><<mainlink>></b>', 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) {
+
function copyStructure(oldStructure, newStructure) {
pg.structures[newStructure]={};
+
pg.structures[newStructure] = {};
for (var prop in pg.structures[oldStructure]) {
+
for (var prop in pg.structures[oldStructure]) {
pg.structures[newStructure][prop]=pg.structures[oldStructure][prop];
+
pg.structures[newStructure][prop] = pg.structures[oldStructure][prop];
}
+
}
}
+
}
 
 
copyStructure('original', 'nostalgia');
 
pg.structures.nostalgia.popupTopLinks=function(x)  {
 
var str='';
 
str += '<b><<mainlink|shortcut= >></b>';
 
  
// user links
+
copyStructure('original', 'nostalgia');
// contribs - log - count - email - block
+
pg.structures.nostalgia.popupTopLinks = function (x) {
// count only if applicable; block only if popupAdminLinks
+
var str = '';
str += 'if(user){<br><<contribs|shortcut=c>>';
+
str += '<b><<mainlink|shortcut= >></b>';
str+='if(wikimedia){*<<count|shortcut=#>>}';
 
str+='if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>}}';
 
  
// editing links
+
// user links
// talkpage  -> edit|new - history - un|watch - article|edit
+
// contribs - log - count - email - block
// other page -> edit - history - un|watch - talk|edit|new
+
// count only if applicable; block only if popupAdminLinks
var editstr='<<edit|shortcut=e>>';
+
str += 'if(user){<br><<contribs|shortcut=c>>';
var editOldidStr='if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' +
+
str += 'if(wikimedia){*<<count|shortcut=#>>}';
editstr + '}';
+
str += 'if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>}}';
var historystr='<<history|shortcut=h>>';
 
var watchstr='<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
 
  
str += '<br>if(talk){' +
+
// editing links
editOldidStr+'|<<new|shortcut=+>>' + '*' + historystr+'*'+watchstr + '*' +
+
// talkpage  -> edit|new - history - un|watch - article|edit
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
+
// other page -> edit - history - un|watch - talk|edit|new
'}else{' + // not a talk page
+
var editstr = '<<edit|shortcut=e>>';
editOldidStr + '*' + historystr + '*' + watchstr + '*' +
+
var editOldidStr =
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
+
'if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' +
 +
editstr +
 +
'}';
 +
var historystr = '<<history|shortcut=h>>';
 +
var watchstr = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
  
// misc links
+
str +=
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>';
+
'<br>if(talk){' +
str += 'if(admin){<br>}else{*}<<move|shortcut=m>>';
+
editOldidStr +
 +
'|<<new|shortcut=+>>' +
 +
'*' +
 +
historystr +
 +
'*' +
 +
watchstr +
 +
'*' +
 +
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
 +
'}else{' + // not a talk page
 +
editOldidStr +
 +
'*' +
 +
historystr +
 +
'*' +
 +
watchstr +
 +
'*' +
 +
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
  
// admin links
+
// misc links
str += 'if(admin){*<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*' +
+
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>';
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>}';
+
str += 'if(admin){<br>}else{*}<<move|shortcut=m>>';
return navlinkStringToHTML(str, x.article, x.params);
 
};
 
pg.structures.nostalgia.popupRedirTopLinks=pg.structures.nostalgia.popupTopLinks;
 
  
/** -- fancy -- **/
+
// admin links
copyStructure('original', 'fancy');
+
str +=
pg.structures.fancy.popupTitle=function (x) {
+
'if(admin){*<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*' +
return navlinkStringToHTML('<font size=+0><<mainlink>></font>',x.article,x.params);
+
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>}';
};
+
return navlinkStringToHTML(str, x.article, x.params);
pg.structures.fancy.popupTopLinks=function(x) {
+
};
var hist='<<history|shortcut=h|hist>>|<<lastEdit|shortcut=/|last>>if(mainspace_en){|<<editors|shortcut=E|eds>>}';
+
pg.structures.nostalgia.popupRedirTopLinks = pg.structures.nostalgia.popupTopLinks;
var watch='<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
 
var move='<<move|shortcut=m|move>>';
 
return navlinkStringToHTML('if(talk){' +
 
  '<<edit|shortcut=e>>|<<new|shortcut=+|+>>*' + hist + '*' +
 
  '<<article|shortcut=a>>|<<editArticle|edit>>' + '*' + watch + '*' + move +
 
  '}else{<<edit|shortcut=e>>*' + hist +
 
  '*<<talk|shortcut=t|>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>' +
 
  '*' + watch + '*' + move+'}<br>', x.article, x.params);
 
};
 
pg.structures.fancy.popupOtherLinks=function(x) {
 
var admin='<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*<<undelete|undeleteShort>>|<<delete|shortcut=d|del>>';
 
var user='<<contribs|shortcut=c>>if(wikimedia){|<<count|shortcut=#|#>>}';
 
user+='if(ipuser){|<<arin>>}else{*<<email|shortcut=E|'+
 
popupString('email')+'>>}if(admin){*<<block|shortcut=b>>}';
 
  
var normal='<<whatLinksHere|shortcut=l|links here>>*<<relatedChanges|shortcut=r|related>>';
+
/** -- fancy -- **/
return navlinkStringToHTML('<br>if(user){' + user + '*}if(admin){'+admin+'if(user){<br>}else{*}}' + normal,
+
copyStructure('original', 'fancy');
  x.article, x.params);
+
pg.structures.fancy.popupTitle = function (x) {
};
+
return navlinkStringToHTML('<font size=+0><<mainlink>></font>', x.article, x.params);
pg.structures.fancy.popupRedirTitle=pg.structures.fancy.popupTitle;
+
};
pg.structures.fancy.popupRedirTopLinks=pg.structures.fancy.popupTopLinks;
+
pg.structures.fancy.popupTopLinks = function (x) {
pg.structures.fancy.popupRedirOtherLinks=pg.structures.fancy.popupOtherLinks;
+
var hist =
 +
'<<history|shortcut=h|hist>>|<<lastEdit|shortcut=/|last>>|<<editors|shortcut=E|eds>>';
 +
var watch = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
 +
var move = '<<move|shortcut=m|move>>';
 +
return navlinkStringToHTML(
 +
'if(talk){' +
 +
'<<edit|shortcut=e>>|<<new|shortcut=+|+>>*' +
 +
hist +
 +
'*' +
 +
'<<article|shortcut=a>>|<<editArticle|edit>>' +
 +
'*' +
 +
watch +
 +
'*' +
 +
move +
 +
'}else{<<edit|shortcut=e>>*' +
 +
hist +
 +
'*<<talk|shortcut=t|>>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>' +
 +
'*' +
 +
watch +
 +
'*' +
 +
move +
 +
'}<br>',
 +
x.article,
 +
x.params
 +
);
 +
};
 +
pg.structures.fancy.popupOtherLinks = function (x) {
 +
var admin =
 +
'<<unprotect|unprotectShort>>|<<protect|shortcut=p>>*<<undelete|undeleteShort>>|<<delete|shortcut=d|del>>';
 +
var user = '<<contribs|shortcut=c>>if(wikimedia){|<<count|shortcut=#|#>>}';
 +
user +=
 +
'if(ipuser){|<<arin>>}else{*<<email|shortcut=E|' +
 +
popupString('email') +
 +
'>>}if(admin){*<<block|shortcut=b>>}';
  
 +
var normal = '<<whatLinksHere|shortcut=l|links here>>*<<relatedChanges|shortcut=r|related>>';
 +
return navlinkStringToHTML(
 +
'<br>if(user){' + user + '*}if(admin){' + admin + 'if(user){<br>}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 -- **/
+
/** -- fancy2 -- **/
// hack for [[User:MacGyverMagic]]
+
// hack for [[User:MacGyverMagic]]
copyStructure('fancy', 'fancy2');
+
copyStructure('fancy', 'fancy2');
pg.structures.fancy2.popupTopLinks=function(x) { // hack out the <br> at the end and put one at the beginning
+
pg.structures.fancy2.popupTopLinks = function (x) {
return '<br>'+pg.structures.fancy.popupTopLinks(x).replace(RegExp('<br>$','i'),'');
+
// hack out the <br> at the end and put one at the beginning
};
+
return '<br>' + pg.structures.fancy.popupTopLinks(x).replace(RegExp('<br>$', 'i'), '');
pg.structures.fancy2.popupLayout=function () { // move toplinks to after the title
+
};
return ['popupError', 'popupImage', 'popupTitle', 'popupData', 'popupTopLinks', 'popupOtherLinks',
+
pg.structures.fancy2.popupLayout = function () {
'popupRedir', ['popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks'],
+
// move toplinks to after the title
'popupMiscTools', ['popupRedlink'],
+
return [
'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab'];
+
'popupError',
};
+
'popupImage',
 +
'popupTitle',
 +
'popupUserData',
 +
'popupData',
 +
'popupTopLinks',
 +
'popupOtherLinks',
 +
'popupRedir',
 +
[
 +
'popupWarnRedir',
 +
'popupRedirTopLinks',
 +
'popupRedirTitle',
 +
'popupRedirData',
 +
'popupRedirOtherLinks',
 +
],
 +
'popupMiscTools',
 +
['popupRedlink'],
 +
'popupPrePreviewSep',
 +
'popupPreview',
 +
'popupSecondPreview',
 +
'popupPreviewMore',
 +
'popupPostPreview',
 +
'popupFixDab',
 +
];
 +
};
  
/** -- menus -- **/
+
/** -- menus -- **/
copyStructure('original', 'menus');
+
copyStructure('original', 'menus');
pg.structures.menus.popupLayout=function () {
+
pg.structures.menus.popupLayout = function () {
return ['popupError', 'popupImage', 'popupTopLinks', 'popupTitle', 'popupOtherLinks',
+
return [
'popupRedir', ['popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks'],
+
'popupError',
'popupData', 'popupMiscTools', ['popupRedlink'],
+
'popupImage',
'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab'];
+
'popupTopLinks',
};
+
'popupTitle',
function toggleSticky(uid) {
+
'popupOtherLinks',
var popDiv=document.getElementById('navpopup_maindiv'+uid);
+
'popupRedir',
if (!popDiv) { return; }
+
[
if (!popDiv.navpopup.sticky) { popDiv.navpopup.stick(); }
+
'popupWarnRedir',
else {
+
'popupRedirTopLinks',
popDiv.navpopup.unstick();
+
'popupRedirTitle',
popDiv.navpopup.hide();
+
'popupRedirData',
}
+
'popupRedirOtherLinks',
}
+
],
pg.structures.menus.popupTopLinks = function (x, shorter) {
+
'popupUserData',
// FIXME maybe this stuff should be cached
+
'popupData',
var s=[];
+
'popupMiscTools',
var dropdiv='<div class="popup_drop">';
+
['popupRedlink'],
var enddiv='</div>';
+
'popupPrePreviewSep',
var endspan='</span>';
+
'popupPreview',
var hist='<<history|shortcut=h>>';
+
'popupSecondPreview',
if (!shorter) { hist = '<menurow>' + hist +
+
'popupPreviewMore',
'|<<historyfeed|rss>>if(mainspace_en){|<<editors|shortcut=E>>}</menurow>'; }
+
'popupPostPreview',
var lastedit='<<lastEdit|shortcut=/|show last edit>>';
+
'popupFixDab',
var thank='if(diff){<<thank|send thanks>>}';
+
];
var jsHistory='<<lastContrib|last set of edits>><<sinceMe|changes since mine>>';
+
};
var linkshere='<<whatLinksHere|shortcut=l|what links here>>';
 
var related='<<relatedChanges|shortcut=r|related changes>>';
 
var search='<menurow><<search|shortcut=s>>if(wikimedia){|<<globalsearch|shortcut=g|global>>}' +
 
'|<<google|shortcut=G|web>></menurow>';
 
var watch='<menurow><<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>></menurow>';
 
var protect='<menurow><<unprotect|unprotectShort>>|' +
 
'<<protect|shortcut=p>>|<<protectlog|log>></menurow>';
 
var del='<menurow><<undelete|undeleteShort>>|<<delete|shortcut=d>>|' +
 
'<<deletelog|log>></menurow>';
 
var move='<<move|shortcut=m|move page>>';
 
var nullPurge='<menurow><<nullEdit|shortcut=n|null edit>>|<<purge|shortcut=P>></menurow>';
 
var viewOptions='<menurow><<view|shortcut=v>>|<<render|shortcut=S>>|<<raw>></menurow>';
 
var editRow='if(oldid){' +
 
'<menurow><<edit|shortcut=e>>|<<editOld|shortcut=e|this&nbsp;revision>></menurow>' +
 
'<menurow><<revert|shortcut=v>>|<<undo>></menurow>' + '}else{<<edit|shortcut=e>>}';
 
var markPatrolled='if(rcid){<<markpatrolled|mark patrolled>>}';
 
var newTopic='if(talk){<<new|shortcut=+|new topic>>}';
 
var protectDelete='if(admin){' + protect + del + '}';
 
  
if (getValueOf('popupActionsMenu')) {
+
pg.structures.menus.popupTopLinks = function (x, shorter) {
s.push( '<<mainlink>>*' + dropdiv + menuTitle('actions'));
+
// FIXME maybe this stuff should be cached
} else {
+
var s = [];
s.push( dropdiv + '<<mainlink>>');
+
var dropdiv = '<div class="popup_drop">';
}
+
var enddiv = '</div>';
s.push( '<menu>');
+
var hist = '<<history|shortcut=h>>';
s.push( editRow + markPatrolled + newTopic + hist + lastedit + thank );
+
if (!shorter) {
if (!shorter) { s.push(jsHistory); }
+
hist = '<menurow>' + hist + '|<<historyfeed|rss>>|<<editors|shortcut=E>></menurow>';
s.push( move + linkshere + related);
+
}
if (!shorter) { s.push(nullPurge + search); }
+
var lastedit = '<<lastEdit|shortcut=/|show last edit>>';
if (!shorter) { s.push(viewOptions); }
+
var thank = 'if(diff){<<thank|send thanks>>}';
s.push('<hr />' + watch + protectDelete);
+
var jsHistory = '<<lastContrib|last set of edits>><<sinceMe|changes since mine>>';
s.push('<hr />' +
+
var linkshere = '<<whatLinksHere|shortcut=l|what links here>>';
  'if(talk){<<article|shortcut=a|view article>><<editArticle|edit article>>}' +
+
var related = '<<relatedChanges|shortcut=r|related changes>>';
  'else{<<talk|shortcut=t|talk page>><<editTalk|edit talk>>' +
+
var search =
  '<<newTalk|shortcut=+|new topic>>}</menu>' + enddiv);
+
'<menurow><<search|shortcut=s>>if(wikimedia){|<<globalsearch|shortcut=g|global>>}' +
 +
'|<<google|shortcut=G|web>></menurow>';
 +
var watch = '<menurow><<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>></menurow>';
 +
var protect =
 +
'<menurow><<unprotect|unprotectShort>>|' +
 +
'<<protect|shortcut=p>>|<<protectlog|log>></menurow>';
 +
var del =
 +
'<menurow><<undelete|undeleteShort>>|<<delete|shortcut=d>>|' + '<<deletelog|log>></menurow>';
 +
var move = '<<move|shortcut=m|move page>>';
 +
var nullPurge = '<menurow><<nullEdit|shortcut=n|null edit>>|<<purge|shortcut=P>></menurow>';
 +
var viewOptions = '<menurow><<view|shortcut=v>>|<<render|shortcut=S>>|<<raw>></menurow>';
 +
var editRow =
 +
'if(oldid){' +
 +
'<menurow><<edit|shortcut=e>>|<<editOld|shortcut=e|this&nbsp;revision>></menurow>' +
 +
'<menurow><<revert|shortcut=v>>|<<undo>></menurow>' +
 +
'}else{<<edit|shortcut=e>>}';
 +
var markPatrolled = 'if(rcid){<<markpatrolled|mark patrolled>>}';
 +
var newTopic = 'if(talk){<<new|shortcut=+|new topic>>}';
 +
var protectDelete = 'if(admin){' + protect + del + '}';
  
// user menu starts here
+
if (getValueOf('popupActionsMenu')) {
var email='<<email|shortcut=E|email user>>';
+
s.push('<<mainlink>>*' + dropdiv + menuTitle('actions'));
var contribs= 'if(wikimedia){<menurow>}<<contribs|shortcut=c|contributions>>if(wikimedia){</menurow>}' +
+
} else {
'if(admin){<menurow><<deletedContribs>></menurow>}';
+
s.push(dropdiv + '<<mainlink>>');
 +
}
 +
s.push('<menu>');
 +
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('<hr />' + watch + protectDelete);
 +
s.push(
 +
'<hr />' +
 +
'if(talk){<<article|shortcut=a|view article>><<editArticle|edit article>>}' +
 +
'else{<<talk|shortcut=t|talk page>><<editTalk|edit talk>>' +
 +
'<<newTalk|shortcut=+|new topic>>}</menu>' +
 +
enddiv
 +
);
  
 +
// user menu starts here
 +
var email = '<<email|shortcut=E|email user>>';
 +
var contribs =
 +
'if(wikimedia){<menurow>}<<contribs|shortcut=c|contributions>>if(wikimedia){</menurow>}' +
 +
'if(admin){<menurow><<deletedContribs>></menurow>}';
  
s.push('if(user){*' + dropdiv + menuTitle('user'));
+
s.push('if(user){*' + dropdiv + menuTitle('user'));
s.push('<menu>');
+
s.push('<menu>');
s.push('<menurow><<userPage|shortcut=u|user&nbsp;page>>|<<userSpace|space>></menurow>');
+
s.push('<menurow><<userPage|shortcut=u|user&nbsp;page>>|<<userSpace|space>></menurow>');
s.push('<<userTalk|shortcut=t|user talk>><<editUserTalk|edit user talk>>' +
+
s.push(
  '<<newUserTalk|shortcut=+|leave comment>>');
+
'<<userTalk|shortcut=t|user talk>><<editUserTalk|edit user talk>>' +
if(!shorter) { s.push( 'if(ipuser){<<arin>>}else{' + email + '}' ); }
+
'<<newUserTalk|shortcut=+|leave comment>>'
else { s.push( 'if(ipuser){}else{' + email + '}' ); }
+
);
s.push('<hr />' + contribs + '<<userlog|shortcut=L|user log>>');
+
if (!shorter) {
s.push('if(wikimedia){<<count|shortcut=#|edit counter>>}');
+
s.push('if(ipuser){<<arin>>}else{' + email + '}');
s.push('if(admin){<menurow><<unblock|unblockShort>>|<<block|shortcut=b|block user>></menurow>}');
+
} else {
s.push('<<blocklog|shortcut=B|block log>>' + getValueOf('popupExtraUserMenu'));
+
s.push('if(ipuser){}else{' + email + '}');
s.push('</menu>' + enddiv + '}');
+
}
 +
s.push('<hr />' + contribs + '<<userlog|shortcut=L|user log>>');
 +
s.push('if(wikimedia){<<count|shortcut=#|edit counter>>}');
 +
s.push(
 +
'if(admin){<menurow><<unblock|unblockShort>>|<<block|shortcut=b|block user>></menurow>}'
 +
);
 +
s.push('<<blocklog|shortcut=B|block log>>');
 +
s.push('</menu>' + enddiv + '}');
  
// popups menu starts here
+
// popups menu starts here
if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) {
+
if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) {
x.navpop.hasPopupMenu=true;
+
x.navpop.hasPopupMenu = true;
s.push('*' + dropdiv + menuTitle('popupsMenu') + '<menu>');
+
s.push('*' + dropdiv + menuTitle('popupsMenu') + '<menu>');
s.push('<<togglePreviews|toggle previews>>');
+
s.push('<<togglePreviews|toggle previews>>');
s.push('<<purgePopups|reset>>');
+
s.push('<<purgePopups|reset>>');
s.push('<<disablePopups|disable>>');
+
s.push('<<disablePopups|disable>>');
s.push('</menu>'+enddiv);
+
s.push('</menu>' + enddiv);
}
+
}
return navlinkStringToHTML(s.join(''), x.article, x.params);
+
return navlinkStringToHTML(s.join(''), x.article, x.params);
};
+
};
  
function menuTitle(s) {
+
function menuTitle(s) {
return '<a href="#" noPopup=1>' + popupString(s) + '</a>';
+
return '<a href="#" noPopup=1>' + popupString(s) + '</a>';
}
+
}
  
pg.structures.menus.popupRedirTitle=pg.structures.menus.popupTitle;
+
pg.structures.menus.popupRedirTitle = pg.structures.menus.popupTitle;
pg.structures.menus.popupRedirTopLinks=pg.structures.menus.popupTopLinks;
+
pg.structures.menus.popupRedirTopLinks = pg.structures.menus.popupTopLinks;
  
copyStructure('menus', 'shortmenus');
+
copyStructure('menus', 'shortmenus');
pg.structures.shortmenus.popupTopLinks=function(x) {
+
pg.structures.shortmenus.popupTopLinks = function (x) {
return pg.structures.menus.popupTopLinks(x,true);
+
return pg.structures.menus.popupTopLinks(x, true);
};
+
};
pg.structures.shortmenus.popupRedirTopLinks=pg.structures.shortmenus.popupTopLinks;
+
pg.structures.shortmenus.popupRedirTopLinks = pg.structures.shortmenus.popupTopLinks;
  
copyStructure('shortmenus', 'dabshortmenus');
+
//</NOLITE>
pg.structures.dabshortmenus.popupLayout=function () {
+
pg.structures.lite = {};
return ['popupError', 'popupImage', 'popupTopLinks', 'popupTitle', 'popupOtherLinks',
+
pg.structures.lite.popupLayout = function () {
'popupRedir', ['popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks'],
+
return ['popupTitle', 'popupPreview'];
'popupData', 'popupMiscTools', ['popupRedlink'], 'popupFixDab',
+
};
'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview'];
+
pg.structures.lite.popupTitle = function (x) {
};
+
log(x.article + ': structures.lite.popupTitle');
 +
//return navlinkStringToHTML('<b><<mainlink>></b>',x.article,x.params);
 +
return '<div><span class="popup_mainlink"><b>' + x.article.toString() + '</b></span></div>';
 +
};
 +
// ENDFILE: structures.js
  
copyStructure('menus', 'dabmenus');
+
// STARTFILE: autoedit.js
pg.structures.dabmenus.popupLayout=pg.structures.dabshortmenus.popupLayout;
+
//<NOLITE>
 +
function substitute(data, cmdBody) {
 +
// alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags);
 +
var fromRe = RegExp(cmdBody.from, cmdBody.flags);
 +
return data.replace(fromRe, cmdBody.to);
 +
}
  
 
+
function execCmds(data, cmdList) {
//</NOLITE>
+
for (var i = 0; i < cmdList.length; ++i) {
pg.structures.lite={};
+
data = cmdList[i].action(data, cmdList[i]);
pg.structures.lite.popupLayout=function () {
+
}
return ['popupTitle', 'popupPreview' ];
+
return data;
};
 
pg.structures.lite.popupTitle=function (x) {
 
log (x.article + ': structures.lite.popupTitle');
 
//return navlinkStringToHTML('<b><<mainlink>></b>',x.article,x.params);
 
return '<div><span class="popup_mainlink"><b>' + x.article.toString() + '</b></span></div>';
 
};
 
// ENDFILE: structures.js
 
// STARTFILE: autoedit.js
 
//<NOLITE>
 
function substitute(data,cmdBody) {
 
// alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags);
 
var fromRe=RegExp(cmdBody.from, cmdBody.flags);
 
return data.replace(fromRe, cmdBody.to);
 
}
 
 
 
function execCmds(data, cmdList) {
 
for (var i=0; i<cmdList.length; ++i) {
 
data=cmdList[i].action(data, cmdList[i]);
 
 
}
 
}
return data;
 
}
 
  
function parseCmd(str) {
+
function parseCmd(str) {
// returns a list of commands
+
// returns a list of commands
if (!str.length) { return []; }
+
if (!str.length) {
var p=false;
+
return [];
switch (str.charAt(0)) {
+
}
case 's':
+
var p = false;
p=parseSubstitute(str);
+
switch (str.charAt(0)) {
break;
+
case 's':
default:
+
p = parseSubstitute(str);
 +
break;
 +
default:
 +
return false;
 +
}
 +
if (p) {
 +
return [p].concat(parseCmd(p.remainder));
 +
}
 
return false;
 
return false;
 
}
 
}
if (p) { return [p].concat(parseCmd(p.remainder)); }
 
return false;
 
}
 
  
function unEscape(str, sep) {
+
// FIXME: Only used once here, confusing with native (and more widely-used) unescape, should probably be replaced
return str.split('\\\\').join('\\').split('\\'+sep).join(sep).split('\\n').join('\n');
+
// 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
  
function parseSubstitute(str) {
+
var from, to, flags, tmp;
// takes a string like s/a/b/flags;othercmds and parses it
 
  
var from,to,flags,tmp;
+
if (str.length < 4) {
 +
return false;
 +
}
 +
var sep = str.charAt(1);
 +
str = str.substring(2);
  
if (str.length<4) { return false; }
+
tmp = skipOver(str, sep);
var sep=str.charAt(1);
+
if (tmp) {
str=str.substring(2);
+
from = tmp.segment;
 +
str = tmp.remainder;
 +
} else {
 +
return false;
 +
}
  
tmp=skipOver(str,sep);
+
tmp = skipOver(str, sep);
if (tmp) { from=tmp.segment; str=tmp.remainder; }
+
if (tmp) {
else { return false; }
+
to = tmp.segment;
 +
str = tmp.remainder;
 +
} else {
 +
return false;
 +
}
  
tmp=skipOver(str,sep);
+
flags = '';
if (tmp) { to=tmp.segment; str=tmp.remainder; }
+
if (str.length) {
else { return false; }
+
tmp = skipOver(str, ';') || skipToEnd(str, ';');
 +
if (tmp) {
 +
flags = tmp.segment;
 +
str = tmp.remainder;
 +
}
 +
}
  
flags='';
+
return {
if (str.length) {
+
action: substitute,
tmp=skipOver(str,';') || skipToEnd(str, ';');
+
from: from,
if (tmp) {flags=tmp.segment; str=tmp.remainder; }
+
to: to,
 +
flags: flags,
 +
remainder: str,
 +
};
 
}
 
}
  
return {action: substitute, from: from, to: to, flags: flags, remainder: str};
+
function skipOver(str, sep) {
 +
var endSegment = findNext(str, sep);
 +
if (endSegment < 0) {
 +
return false;
 +
}
 +
var segment = unEscape(str.substring(0, endSegment), sep);
 +
return { segment: segment, remainder: str.substring(endSegment + 1) };
 +
}
  
}
+
/*eslint-disable*/
 
+
function skipToEnd(str, sep) {
function skipOver(str,sep) {
+
return { segment: str, remainder: '' };
var endSegment=findNext(str,sep);
 
if (endSegment<0) { return false; }
 
var segment=unEscape(str.substring(0,endSegment), sep);
 
return {segment: segment, remainder: str.substring(endSegment+1)};
 
}
 
 
 
function skipToEnd(str,sep) {
 
return {segment: str, remainder: ''};
 
}
 
 
 
function findNext(str, ch) {
 
for (var i=0; i<str.length; ++i) {
 
if (str.charAt(i)=='\\') { i+=2; }
 
if (str.charAt(i)==ch) { return i; }
 
 
}
 
}
return -1;
+
/*eslint-enable */
}
 
  
function setCheckbox(param, box) {
+
function findNext(str, ch) {
var val=mw.util.getParamValue(param);
+
for (var i = 0; i < str.length; ++i) {
if (val) {
+
if (str.charAt(i) == '\\') {
switch (val) {
+
i += 2;
case '1': case 'yes': case 'true':
+
}
box.checked=true;
+
if (str.charAt(i) == ch) {
break;
+
return i;
case '0': case 'no':  case 'false':
+
}
box.checked=false;
 
 
}
 
}
 +
return -1;
 
}
 
}
}
 
  
function autoEdit() {
+
function setCheckbox(param, box) {
if (!setupPopups.completed) { setupPopups(); }
+
var val = mw.util.getParamValue(param);
if (!mw.config.get('wgEnableAPI') || mw.util.getParamValue('autoimpl') !== popupString('autoedit_version') ) { return false; }
+
if (val) {
if (mw.util.getParamValue('autowatchlist') && mw.util.getParamValue('actoken')===autoClickToken()) {
+
switch (val) {
pg.fn.modifyWatchlist(mw.util.getParamValue('title'), mw.util.getParamValue('action'));
+
case '1':
}
+
case 'yes':
if (!document.editform) { return false; }
+
case 'true':
if (autoEdit.alreadyRan) { return false; }
+
box.checked = true;
autoEdit.alreadyRan=true;
+
break;
var cmdString=mw.util.getParamValue('autoedit');
+
case '0':
if (cmdString) {
+
case 'no':
try {
+
case 'false':
var editbox=document.editform.wpTextbox1;
+
box.checked = false;
var cmdList=parseCmd(cmdString);
 
var input=editbox.value;
 
var output=execCmds(input, cmdList);
 
editbox.value=output;
 
} catch (dang) { return; }
 
// wikEd user script compatibility
 
if (typeof(wikEdUseWikEd) != 'undefined') {
 
if (wikEdUseWikEd === true) {
 
WikEdUpdateFrame();
 
 
}
 
}
 
}
 
}
 
}
 
}
setCheckbox('autominor', document.editform.wpMinoredit);
 
setCheckbox('autowatch', document.editform.wpWatchthis);
 
  
var rvid = mw.util.getParamValue('autorv');
+
function autoEdit() {
if (rvid) {
+
setupPopups(function () {
var url=pg.wiki.apiwikibase + '?action=query&format=json&prop=revisions&revids='+rvid;
+
if (mw.util.getParamValue('autoimpl') !== popupString('autoedit_version')) {
startDownload(url, null, autoEdit2);
+
return false;
} else { autoEdit2(); }
+
}
}
+
if (
 +
mw.util.getParamValue('autowatchlist') &&
 +
mw.util.getParamValue('actoken') === autoClickToken()
 +
) {
 +
pg.fn.modifyWatchlist(mw.util.getParamValue('title'), mw.util.getParamValue('action'));
 +
}
 +
if (!document.editform) {
 +
return false;
 +
}
 +
if (autoEdit.alreadyRan) {
 +
return false;
 +
}
 +
autoEdit.alreadyRan = true;
 +
var cmdString = mw.util.getParamValue('autoedit');
 +
if (cmdString) {
 +
try {
 +
var editbox = document.editform.wpTextbox1;
 +
var cmdList = parseCmd(cmdString);
 +
var input = editbox.value;
 +
var output = execCmds(input, cmdList);
 +
editbox.value = output;
 +
} catch (dang) {
 +
return;
 +
}
 +
// wikEd user script compatibility
 +
if (typeof wikEdUseWikEd != 'undefined') {
 +
if (wikEdUseWikEd === true) {
 +
WikEdUpdateFrame();
 +
}
 +
}
 +
}
 +
setCheckbox('autominor', document.editform.wpMinoredit);
 +
setCheckbox('autowatch', document.editform.wpWatchthis);
  
function autoEdit2(d) {
+
var rvid = mw.util.getParamValue('autorv');
var summary=mw.util.getParamValue('autosummary');
+
if (rvid) {
var summaryprompt=mw.util.getParamValue('autosummaryprompt');
+
var url =
var summarynotice='';
+
pg.wiki.apiwikibase +
if (d && d.data && mw.util.getParamValue('autorv')) {
+
'?action=query&format=json&formatversion=2&prop=revisions&revids=' +
var s = getRvSummary(summary, d.data);
+
rvid;
if (s === false) {
+
startDownload(url, null, autoEdit2);
summaryprompt=true;
+
} else {
summarynotice=popupString('Failed to get revision information, please edit manually.\n\n');
+
autoEdit2();
summary = simplePrintf(summary, [mw.util.getParamValue('autorv'), '(unknown)', '(unknown)']);
+
}
} else { summary = s; }
+
});
}
 
if (summaryprompt) {
 
var txt= summarynotice +
 
popupString('Enter a non-empty edit summary or press cancel to abort');
 
var response=prompt(txt, summary);
 
if (response) { summary=response; }
 
else { return; }
 
 
}
 
}
if (summary) { document.editform.wpSummary.value=summary; }
 
// Attempt to avoid possible premature clicking of the save button
 
// (maybe delays in updates to the DOM are to blame?? or a red herring)
 
setTimeout(autoEdit3, 100);
 
}
 
  
function autoClickToken() {
+
function autoEdit2(d) {
return mw.user.sessionId();
+
var summary = mw.util.getParamValue('autosummary');
}
+
var summaryprompt = mw.util.getParamValue('autosummaryprompt');
 
+
var summarynotice = '';
function autoEdit3() {
+
if (d && d.data && mw.util.getParamValue('autorv')) {
if( mw.util.getParamValue('actoken') != autoClickToken()) { return; }
+
var s = getRvSummary(summary, d.data);
 
+
if (s === false) {
var btn=mw.util.getParamValue('autoclick');
+
summaryprompt = true;
if (btn) {
+
summarynotice = popupString(
if (document.editform && document.editform[btn]) {
+
'Failed to get revision information, please edit manually.\n\n'
var button=document.editform[btn];
+
);
var msg=tprintf('The %s button has been automatically clicked. Please wait for the next page to load.',
+
summary = simplePrintf(summary, [
[ button.value ]);
+
mw.util.getParamValue('autorv'),
bannerMessage(msg);
+
'(unknown)',
document.title='('+document.title+')';
+
'(unknown)',
button.click();
+
]);
} else {
+
} else {
alert(tprintf('Could not find button %s. Please check the settings in your javascript file.',
+
summary = s;
  [ btn ]));
+
}
 +
}
 +
if (summaryprompt) {
 +
var txt =
 +
summarynotice + popupString('Enter a non-empty edit summary or press cancel to abort');
 +
var response = prompt(txt, summary);
 +
if (response) {
 +
summary = response;
 +
} else {
 +
return;
 +
}
 +
}
 +
if (summary) {
 +
document.editform.wpSummary.value = summary;
 
}
 
}
 +
// Attempt to avoid possible premature clicking of the save button
 +
// (maybe delays in updates to the DOM are to blame?? or a red herring)
 +
setTimeout(autoEdit3, 100);
 
}
 
}
}
 
  
function bannerMessage(s) {
+
function autoClickToken() {
var headings=document.getElementsByTagName('h1');
+
return mw.user.sessionId();
if (headings) {
 
var div=document.createElement('div');
 
div.innerHTML='<font size=+1><b>' + s + '</b></font>';
 
headings[0].parentNode.insertBefore(div, headings[0]);
 
 
}
 
}
}
 
  
function getRvSummary(template, json) {
+
function autoEdit3() {
try {
+
if (mw.util.getParamValue('actoken') != autoClickToken()) {
var o=getJsObj(json);
+
return;
var edit = anyChild(o.query.pages).revisions[0];
+
}
var timestamp = edit.timestamp.split(/[A-Z]/g).join(' ').replace(/^ *| *$/g, '');
+
 
return simplePrintf(template, [edit.revid, timestamp, edit.userhidden === undefined ? edit.user : '(hidden)']);
+
var btn = mw.util.getParamValue('autoclick');
} catch (badness) {
+
if (btn) {
return false;
+
if (document.editform && document.editform[btn]) {
 +
var button = document.editform[btn];
 +
var msg = tprintf(
 +
'The %s button has been automatically clicked. Please wait for the next page to load.',
 +
[button.value]
 +
);
 +
bannerMessage(msg);
 +
document.title = '(' + document.title + ')';
 +
button.click();
 +
} else {
 +
alert(
 +
tprintf('Could not find button %s. Please check the settings in your javascript file.', [
 +
btn,
 +
])
 +
);
 +
}
 +
}
 +
}
 +
 
 +
function bannerMessage(s) {
 +
var headings = document.getElementsByTagName('h1');
 +
if (headings) {
 +
var div = document.createElement('div');
 +
div.innerHTML = '<font size=+1><b>' + pg.escapeQuotesHTML(s) + '</b></font>';
 +
headings[0].parentNode.insertBefore(div, headings[0]);
 +
}
 +
}
 +
 
 +
function getRvSummary(template, json) {
 +
try {
 +
var o = getJsObj(json);
 +
var edit = anyChild(o.query.pages).revisions[0];
 +
var timestamp = edit.timestamp
 +
.split(/[A-Z]/g)
 +
.join(' ')
 +
.replace(/^ *| *$/g, '');
 +
return simplePrintf(template, [
 +
edit.revid,
 +
timestamp,
 +
edit.userhidden ? '(hidden)' : edit.user,
 +
]);
 +
} catch (badness) {
 +
return false;
 +
}
 
}
 
}
}
 
  
//</NOLITE>
+
//</NOLITE>
// ENDFILE: autoedit.js
+
// ENDFILE: autoedit.js
// STARTFILE: downloader.js
 
/**
 
  @fileoverview
 
  {@link Downloader}, a xmlhttprequest wrapper, and helper functions.
 
*/
 
  
/**
+
// STARTFILE: downloader.js
  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
+
* @fileoverview
@type String
+
* {@link Downloader}, a xmlhttprequest wrapper, and helper functions.
*/
+
*/
this.url = url;
+
 
 
/**
 
/**
A universally unique ID number
+
* Creates a new Downloader
@type integer
+
* @constructor
 +
* @class The Downloader class. Create a new instance of this class to download stuff.
 +
* @param {String} url The url to download. This can be omitted and supplied later.
 +
*/
 +
function Downloader(url) {
 +
if (typeof XMLHttpRequest != 'undefined') {
 +
this.http = new XMLHttpRequest();
 +
}
 +
 
 +
/**
 +
* The url to download
 +
* @type String
 +
*/
 +
this.url = url;
 +
 
 +
/**
 +
* A universally unique ID number
 +
* @type integer
 +
*/
 +
this.id = null;
 +
 
 +
/**
 +
* Modification date, to be culled from the incoming headers
 +
* @type Date
 +
* @private
 +
*/
 +
this.lastModified = null;
 +
 
 +
/**
 +
* What to do when the download completes successfully
 +
* @type Function
 +
* @private
 +
*/
 +
this.callbackFunction = null;
 +
 
 +
/**
 +
* What to do on failure
 +
* @type Function
 +
* @private
 +
*/
 +
this.onFailure = null;
 +
 
 +
/**
 +
* Flag set on <code>abort</code>
 +
* @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.id=null;
+
this.async = true;
/**
+
}
Modification date, to be culled from the incoming headers
+
 
@type Date
+
new Downloader();
@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 <code>abort</code>
 
@type boolean
 
*/
 
this.aborted = false;
 
/**
 
  HTTP method. See http://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);
 +
};
  
/** Submits the http request. */
+
/** Aborts the download, setting the <code>aborted</code> field to true.  */
Downloader.prototype.send = function (x) {
+
Downloader.prototype.abort = function () {
if (!this.http) { return null; }
+
if (!this.http) {
return this.http.send(x);
+
return null;
};
+
}
/** Aborts the download, setting the <code>aborted</code> field to true.  */
+
this.aborted = true;
Downloader.prototype.abort = function () {
+
return this.http.abort();
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);
 
};
 
/** Gets the state of the download. */
 
Downloader.prototype.getReadyState=function () {if (!this.http) { return null; } return this.http.readyState;};
 
  
pg.misc.downloadsInProgress = { };
+
/** Returns the downloaded data. */
 +
Downloader.prototype.getData = function () {
 +
if (!this.http) {
 +
return null;
 +
}
 +
return this.http.responseText;
 +
};
  
/** Starts the download.
+
/** Prepares the download. */
Note that setTarget {@link Downloader#setTarget} must be run first
+
Downloader.prototype.setTarget = function () {
*/
+
if (!this.http) {
Downloader.prototype.start=function () {
+
return null;
if (!this.http) { return; }
+
}
pg.misc.downloadsInProgress[this.id] = this;
+
this.http.open(this.method, this.url, this.async);
this.http.send(null);
+
this.http.setRequestHeader('Api-User-Agent', pg.api.userAgent);
};
+
};
  
/** Gets the 'Last-Modified' date from the download headers.
+
/** Gets the state of the download. */
Should be run after the download completes.
+
Downloader.prototype.getReadyState = function () {
Returns <code>null</code> on failure.
+
if (!this.http) {
@return {Date}
+
return null;
*/
 
Downloader.prototype.getLastModifiedDate=function () {
 
if(!this.http) { return null; }
 
var lastmod=null;
 
try {
 
lastmod=this.http.getResponseHeader('Last-Modified');
 
} catch (err) {}
 
if (lastmod) { return new Date(lastmod); }
 
return null;
 
};
 
 
 
/** Sets the callback function.
 
@param {Function} f callback function, called as <code>f(this)</code> on success
 
*/
 
Downloader.prototype.setCallback = function (f) {
 
if(!this.http) { return; }
 
this.http.onreadystatechange = f;
 
};
 
 
 
Downloader.prototype.getStatus = function() { if (!this.http) { return null; } return this.http.status; };
 
 
 
//////////////////////////////////////////////////
 
// helper functions
 
 
 
/** Creates a new {@link Downloader} and prepares it for action.
 
@param {String} url The url to download
 
@param {integer} id The ID of the {@link Downloader} object
 
@param {Function} callback The callback function invoked on success
 
@return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
 
*/
 
function newDownload(url, id, callback, onfailure) {
 
var d=new Downloader(url);
 
if (!d.http) { return 'ohdear'; }
 
d.id=id;
 
d.setTarget();
 
if (!onfailure) {
 
onfailure=2;
 
}
 
var f = function () {
 
if (d.getReadyState() == 4) {
 
delete pg.misc.downloadsInProgress[this.id];
 
try {
 
if ( d.getStatus() == 200 ) {
 
d.data=d.getData();
 
d.lastModified=d.getLastModifiedDate();
 
callback(d);
 
} else if (typeof onfailure == typeof 1) {
 
if (onfailure > 0) {
 
// retry
 
newDownload(url, id, callback, onfailure - 1);
 
}
 
} else if ($.isFunction(onfailure)) {
 
onfailure(d,url,id,callback);
 
}
 
} catch (somerr) { /* ignore it */ }
 
 
}
 
}
 +
return this.http.readyState;
 
};
 
};
d.setCallback(f);
 
return d;
 
}
 
/** Simulates a download from cached data.
 
The supplied data is put into a {@link Downloader} as if it had downloaded it.
 
@param {String} url The url.
 
@param {integer} id The ID.
 
@param {Function} callback The callback, which is invoked immediately as <code>callback(d)</code>,
 
where <code>d</code> is the new {@link Downloader}.
 
@param {String} data The (cached) data.
 
@param {Date} lastModified The (cached) last modified date.
 
*/
 
function fakeDownload(url, id, callback, data, lastModified, owner) {
 
var d=newDownload(url,callback);
 
d.owner=owner;
 
d.id=id; d.data=data;
 
d.lastModified=lastModified;
 
return callback(d);
 
}
 
  
/**
+
pg.misc.downloadsInProgress = {};
  Starts a download.
 
  @param {String} url The url to download
 
  @param {integer} id The ID of the {@link Downloader} object
 
  @param {Function} callback The callback function invoked on success
 
  @return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
 
*/
 
function startDownload(url, id, callback) {
 
var d=newDownload(url, id, callback);
 
if (typeof d == typeof '' ) { return d; }
 
d.start();
 
return d;
 
}
 
  
/**
+
/**
  Aborts all downloads which have been started.
+
* Starts the download.
*/
+
* Note that setTarget {@link Downloader#setTarget} must be run first
function abortAllDownloads() {
+
*/
for ( var x in pg.misc.downloadsInProgress ) {
+
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 <code>null</code> on failure.
 +
* @return {Date}
 +
*/
 +
Downloader.prototype.getLastModifiedDate = function () {
 +
if (!this.http) {
 +
return null;
 +
}
 +
var lastmod = null;
 
try {
 
try {
pg.misc.downloadsInProgress[x].aborted=true;
+
lastmod = this.http.getResponseHeader('Last-Modified');
pg.misc.downloadsInProgress[x].abort();
+
} catch (err) {}
delete pg.misc.downloadsInProgress[x];
+
if (lastmod) {
} catch (e) { }
+
return new Date(lastmod);
}
+
}
}
+
return null;
// ENDFILE: downloader.js
+
};
// STARTFILE: livepreview.js
 
// TODO: location is often not correct (eg relative links in previews)
 
  
/**
+
/**
* InstaView - a Mediawiki to HTML converter in JavaScript
+
* Sets the callback function.
* Version 0.6.1
+
* @param {Function} f callback function, called as <code>f(this)</code> on success
* Copyright (C) Pedro Fayolle 2005-2006
+
*/
* http://en.wikipedia.org/wiki/User:Pilaf
+
Downloader.prototype.setCallback = function (f) {
* Distributed under the BSD license
+
if (!this.http) {
*
+
return;
* Changelog:
+
}
*
+
this.http.onreadystatechange = f;
* 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 <nowiki>
 
* - Parser-based (as opposed to RegExp-based) inline wikicode handling (make it one-pass and bullet-proof)
 
* - Support for templates (through AJAX)
 
* - Support for coloured links (AJAX)
 
*/
 
 
 
 
 
var Insta = {};
 
 
 
function setupLivePreview() {
 
 
 
// options
 
Insta.conf =
 
{
 
baseUrl: '',
 
 
 
user: {},
 
 
 
wiki: {
 
lang: pg.wiki.lang,
 
interwiki: pg.wiki.interwiki,
 
default_thumb_width: 180
 
},
 
  
paths: {
+
Downloader.prototype.getStatus = function () {
articles: pg.wiki.articlePath + '/',
+
if (!this.http) {
// Only used for Insta previews with images. (not in popups)
+
return null;
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']
 
 
}
 
}
 +
return this.http.status;
 
};
 
};
  
// options with default values or backreferences
+
//////////////////////////////////////////////////
Insta.conf.user.name = Insta.conf.user.name || 'Wikipedian';
+
// helper functions
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+
+
* Creates a new {@link Downloader} and prepares it for action.
        '):.*?\\|.*?(?:frame|thumbnail|thumb|none|right|left|center)', 'i');
+
* @param {String} url The url to download
 
+
* @param {integer} id The ID of the {@link Downloader} object
}
+
* @param {Function} callback The callback function invoked on success
 
+
* @return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
 
+
*/
Insta.dump = function(from, to)
+
function newDownload(url, id, callback, onfailure) {
{
+
var d = new Downloader(url);
if (typeof from == 'string') { from = document.getElementById(from); }
+
if (!d.http) {
if (typeof to == 'string') { to = document.getElementById(to); }
+
return 'ohdear';
to.innerHTML = this.convert(from.value);
 
};
 
 
 
Insta.convert = function(wiki)
 
{
 
var ll = (typeof wiki == 'string')? wiki.replace(/\r/g,'').split(/\n/): wiki, // lines of wikicode
 
o  = '', // output
 
p  = 0, // para flag
 
$r; // result of passing a regexp to $()
 
 
 
// some shorthands
 
function remain() { return ll.length; }
 
function sh() { return ll.shift(); } // shift
 
function ps(s) { o += s; } // push
 
 
 
// similar to C's printf, uses ? as placeholders, ?? to escape question marks
 
function f()
 
{
 
var i=1, a=arguments,  f=a[0], o='', c, p;
 
for (; i<a.length; i++) {
 
if ((p=f.indexOf('?'))+1) {
 
// allow character escaping
 
i -= c = f.charAt(p+1)=='?' ? 1 : 0;
 
o += f.substring(0,p) + (c ? '?' : a[i]);
 
f = f.substr(p+1+c);
 
} else { break; }
 
 
}
 
}
return o+f;
+
d.id = id;
 +
d.setTarget();
 +
if (!onfailure) {
 +
onfailure = 2;
 +
}
 +
var f = function () {
 +
if (d.getReadyState() == 4) {
 +
delete pg.misc.downloadsInProgress[this.id];
 +
try {
 +
if (d.getStatus() == 200) {
 +
d.data = d.getData();
 +
d.lastModified = d.getLastModifiedDate();
 +
callback(d);
 +
} else if (typeof onfailure == typeof 1) {
 +
if (onfailure > 0) {
 +
// retry
 +
newDownload(url, id, callback, onfailure - 1);
 +
}
 +
} else if (typeof onfailure === 'function') {
 +
onfailure(d, url, id, callback);
 +
}
 +
} catch (somerr) {
 +
/* ignore it */
 +
}
 +
}
 +
};
 +
d.setCallback(f);
 +
return d;
 +
}
 +
/**
 +
* Simulates a download from cached data.
 +
* The supplied data is put into a {@link Downloader} as if it had downloaded it.
 +
* @param {String} url The url.
 +
* @param {integer} id The ID.
 +
* @param {Function} callback The callback, which is invoked immediately as <code>callback(d)</code>,
 +
* where <code>d</code> is the new {@link Downloader}.
 +
* @param {String} data The (cached) data.
 +
* @param {Date} lastModified The (cached) last modified date.
 +
*/
 +
function fakeDownload(url, id, callback, data, lastModified, owner) {
 +
var d = newDownload(url, callback);
 +
d.owner = owner;
 +
d.id = id;
 +
d.data = data;
 +
d.lastModified = lastModified;
 +
return callback(d);
 
}
 
}
  
function html_entities(s) {
+
/**
return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
+
* Starts a download.
}
+
* @param {String} url The url to download
+
* @param {integer} id The ID of the {@link Downloader} object
// Wiki text parsing to html is a nightmare.
+
* @param {Function} callback The callback function invoked on success
// The below functions deliberately don't escape the ampersand since this would make it more difficult,
+
* @return {String/Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
// 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.
+
function startDownload(url, id, callback) {
// Browsers should all be able to handle it though.
+
var d = newDownload(url, id, callback);
// We also escape significant wikimarkup characters to prevent further matching on the processed text
+
if (typeof d == typeof '') {
function htmlescape_text(s) {
+
return d;
return s.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/:/g,"&#58;").replace(/\[/g,"&#91;").replace(/]/g,"&#93;");
+
}
}
+
d.start();
function htmlescape_attr(s) {
+
return d;
return htmlescape_text(s).replace(/'/g,"&#39;").replace(/"/g,"&quot;");
 
 
}
 
}
  
function max(a,b) { return (a>b)?a:b; }
+
/**
function min(a,b) { return (a<b)?a:b; }
+
* Aborts all downloads which have been started.
 
+
*/
// return the first non matching character position between two strings
+
function abortAllDownloads() {
function str_imatch(a, b)
+
for (var x in pg.misc.downloadsInProgress) {
{
+
try {
for (var i=0, l=min(a.length, b.length); i<l; i++) {
+
pg.misc.downloadsInProgress[x].aborted = true;
if (a.charAt(i)!=b.charAt(i)) { break; }
+
pg.misc.downloadsInProgress[x].abort();
 +
delete pg.misc.downloadsInProgress[x];
 +
} catch (e) {}
 
}
 
}
return i;
 
 
}
 
}
 +
// ENDFILE: downloader.js
  
// compare current line against a string or regexp
+
// STARTFILE: livepreview.js
// if passed a string it will compare only the first string.length characters
+
// TODO: location is often not correct (eg relative links in previews)
// if passed a regexp the result is stored in $r
+
// NOTE: removed md5 and image and math parsing. was broken, lots of bytes.
function $(c) { return (typeof c == 'string') ? (ll[0].substr(0,c.length)==c) : ($r = ll[0].match(c)); }
+
/**
 +
* 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 <nowiki>
 +
* - 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)
 +
*/
  
function $$(c) { return ll[0]==c; } // compare current line against a string
+
var Insta = {};
function _(p) { return ll[0].charAt(p); } // return char at pos p
 
  
function endl(s) { ps(s); sh(); }
+
function setupLivePreview() {
 +
// options
 +
Insta.conf = {
 +
baseUrl: '',
  
function parse_list()
+
user: {},
{
 
var prev='';
 
  
while (remain() && $(/^([*#:;]+)(.*)$/)) {
+
wiki: {
 +
lang: pg.wiki.lang,
 +
interwiki: pg.wiki.interwiki,
 +
default_thumb_width: 180,
 +
},
  
var l_match = $r;
+
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/',
 +
},
  
sh();
+
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',
 +
],
 +
},
 +
};
  
var ipos = str_imatch(prev, l_match[1]);
+
// 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 + '/';
  
// close uncontinued lists
+
// define constants
for (var prevPos=prev.length-1; prevPos >= ipos; prevPos--) {
+
Insta.BLOCK_IMAGE = new RegExp(
 +
'^\\[\\[(?:File|Image|' +
 +
Insta.conf.locale.image +
 +
'):.*?\\|.*?(?:frame|thumbnail|thumb|none|right|left|center)',
 +
'i'
 +
);
 +
}
  
var pi = prev.charAt(prevPos);
+
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);
 +
};
  
if (pi=='*') { ps('</ul>'); }
+
Insta.convert = function (wiki) {
else if (pi=='#') { ps('</ol>'); }
+
var ll = typeof wiki == 'string' ? wiki.replace(/\r/g, '').split(/\n/) : wiki, // lines of wikicode
// close a dl only if the new item is not a dl item (:, ; or empty)
+
o = '', // output
else if($.inArray(l_match[1].charAt(prevPos), ['','*','#'])) { ps('</dl>'); }
+
p = 0, // para flag
}
+
r; // result of passing a regexp to compareLineStringOrReg()
  
// open new lists
+
// some shorthands
for (var matchPos=ipos; matchPos<l_match[1].length; matchPos++) {
+
function remain() {
 +
return ll.length;
 +
}
 +
function sh() {
 +
return ll.shift();
 +
} // shift
 +
function ps(s) {
 +
o += s;
 +
} // push
  
var li = l_match[1].charAt(matchPos);
+
// similar to C's printf, uses ? as placeholders, ?? to escape question marks
 
+
function f() {
if (li=='*') { ps('<ul>'); }
+
var i = 1,
else if (li=='#') { ps('<ol>'); }
+
a = arguments,
// open a new dl only if the prev item is not a dl item (:, ; or empty)
+
f = a[0],
else if ($.inArray(prev.charAt(matchPos), ['','*','#'])) { ps('<dl>'); }
+
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;
 +
}
  
switch (l_match[1].charAt(l_match[1].length-1)) {
+
function html_entities(s) {
 +
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 +
}
  
case '*': case '#':
+
// Wiki text parsing to html is a nightmare.
ps('<li>' + parse_inline_nowiki(l_match[2]));
+
// The below functions deliberately don't escape the ampersand since this would make it more
break;
+
// 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.
case ';':
+
// Browsers should all be able to handle it though. We also escape significant wikimarkup
ps('<dt>');
+
// characters to prevent further matching on the processed text.
 
+
function htmlescape_text(s) {
var dt_match = l_match[2].match(/(.*?)(:.*?)$/);
+
return s
 
+
.replace(/</g, '&lt;')
// handle ;dt :dd format
+
.replace(/>/g, '&gt;')
if (dt_match) {
+
.replace(/:/g, '&#58;')
ps(parse_inline_nowiki(dt_match[1]));
+
.replace(/\[/g, '&#91;')
ll.unshift(dt_match[2]);
+
.replace(/]/g, '&#93;');
 +
}
 +
function htmlescape_attr(s) {
 +
return htmlescape_text(s).replace(/'/g, '&#39;').replace(/"/g, '&quot;');
 +
}
  
} else ps(parse_inline_nowiki(l_match[2]));
+
// 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;
 
break;
 
+
}
case ':':
 
ps('<dd>' + parse_inline_nowiki(l_match[2]));
 
 
}
 
}
 
+
return i;
prev=l_match[1];
 
 
}
 
}
  
// close remaining lists
+
// compare current line against a string or regexp
for (var i=prev.length-1; i>=0; i--) {
+
// if passed a string it will compare only the first string.length characters
ps(f('</?>', (prev.charAt(i)=='*')? 'ul': ((prev.charAt(i)=='#')? 'ol': 'dl')));
+
// 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 parse_table()
+
function compareLineString(c) {
{
+
return ll[0] == c;
endl(f('<table>', $(/^\{\|( .*)$/)? $r[1]: ''));
+
} // compare current line against a string
 +
function charAtPoint(p) {
 +
return ll[0].charAt(p);
 +
} // return char at pos p
  
for (;remain();) if ($('|')) switch (_(1)) {
+
function endl(s) {
case '}':
+
ps(s);
endl('</table>');
+
sh();
return;
 
case '-':
 
endl(f('<tr>', $(/\|-*(.*)/)[1]));
 
break;
 
default:
 
parse_table_data();
 
 
}
 
}
else if ($('!')) { parse_table_data(); }
 
else { sh(); }
 
}
 
  
function parse_table_data()
+
function parse_list() {
{
+
var prev = '';
var td_line, match_i;
+
 
 +
while (remain() && compareLineStringOrReg(/^([*#:;]+)(.*)$/)) {
 +
var l_match = r;
  
// 1: "|+", '|' or '+'
+
sh();
// 2: ??
 
// 3: attributes ??
 
// TODO: finish commenting this regexp
 
var td_match = sh().match(/^(\|\+|\||!)((?:([^[|]*?)\|(?!\|))?(.*))$/);
 
  
if (td_match[1] == '|+') ps('<caption');
+
var ipos = str_imatch(prev, l_match[1]);
else ps('<t' + ((td_match[1]=='|')?'d':'h'));
 
  
if (typeof td_match[3] != 'undefined') {
+
// close uncontinued lists
 +
for (var prevPos = prev.length - 1; prevPos >= ipos; prevPos--) {
 +
var pi = prev.charAt(prevPos);
  
//ps(' ' + td_match[3])
+
if (pi == '*') {
match_i = 4;
+
ps('</ul>');
 +
} else if (pi == '#') {
 +
ps('</ol>');
 +
}
 +
// close a dl only if the new item is not a dl item (:, ; or empty)
 +
else if ($.inArray(l_match[1].charAt(prevPos), ['', '*', '#'])) {
 +
ps('</dl>');
 +
}
 +
}
  
} else match_i = 2;
+
// open new lists
 +
for (var matchPos = ipos; matchPos < l_match[1].length; matchPos++) {
 +
var li = l_match[1].charAt(matchPos);
  
ps('>');
+
if (li == '*') {
 +
ps('<ul>');
 +
} else if (li == '#') {
 +
ps('<ol>');
 +
}
 +
// open a new dl only if the prev item is not a dl item (:, ; or empty)
 +
else if ($.inArray(prev.charAt(matchPos), ['', '*', '#'])) {
 +
ps('<dl>');
 +
}
 +
}
  
if (td_match[1] != '|+') {
+
switch (l_match[1].charAt(l_match[1].length - 1)) {
 +
case '*':
 +
case '#':
 +
ps('<li>' + parse_inline_nowiki(l_match[2]));
 +
break;
  
// use || or !! as a cell separator depending on context
+
case ';':
// NOTE: when split() is passed a regexp make sure to use non-capturing brackets
+
ps('<dt>');
td_line = td_match[match_i].split((td_match[1] == '|')? '||': /(?:\|\||!!)/);
 
  
ps(parse_inline_nowiki(td_line.shift()));
+
var dt_match = l_match[2].match(/(.*?)(:.*?)$/);
  
while (td_line.length) ll.unshift(td_match[1] + td_line.pop());
+
// handle ;dt :dd format
 +
if (dt_match) {
 +
ps(parse_inline_nowiki(dt_match[1]));
 +
ll.unshift(dt_match[2]);
 +
} else ps(parse_inline_nowiki(l_match[2]));
 +
break;
  
} else ps(td_match[match_i]);
+
case ':':
 +
ps('<dd>' + parse_inline_nowiki(l_match[2]));
 +
}
  
var tc = 0, td = [];
+
prev = l_match[1];
 +
}
  
while (remain()) {
+
// close remaining lists
td.push(sh());
+
for (var i = prev.length - 1; i >= 0; i--) {
if ($('|')) {
+
ps(f('</?>', prev.charAt(i) == '*' ? 'ul' : prev.charAt(i) == '#' ? 'ol' : 'dl'));
if (!tc) break; // we're at the outer-most level (no nested tables), skip to td parse
 
else if (_(1)=='}') tc--;
 
 
}
 
}
else if (!tc && $('!')) break;
 
else if ($('{|')) tc++;
 
 
}
 
}
  
if (td.length) ps(Insta.convert(td));
+
function parse_table() {
}
+
endl(f('<table>', compareLineStringOrReg(/^\{\|( .*)$/) ? r[1] : ''));
  
function parse_pre()
+
for (; remain(); )
{
+
if (compareLineStringOrReg('|'))
ps('<pre>');
+
switch (charAtPoint(1)) {
do {
+
case '}':
endl(parse_inline_nowiki(ll[0].substring(1)) + "\n");
+
endl('</table>');
} while (remain() && $(' '));
+
return;
ps('</pre>');
+
case '-':
}
+
endl(f('<tr>', compareLineStringOrReg(/\|-*(.*)/)[1]));
 
+
break;
function parse_block_image()
+
default:
{
+
parse_table_data();
ps(parse_image(sh()));
+
}
}
+
else if (compareLineStringOrReg('!')) {
 
+
parse_table_data();
function parse_image(str)
+
} else {
{
+
sh();
//<NOLITE>
 
// get what's in between "[[Image:" and "]]"
 
var tag = str.substring(str.indexOf(':') + 1, str.length - 2);
 
 
 
var width;
 
var attr = [], filename, caption = '';
 
var thumb=0, frame=0, center=0;
 
var align='';
 
 
 
if (tag.match(/\|/)) {
 
// manage nested links
 
var nesting = 0;
 
var last_attr;
 
for (var i = tag.length-1; i > 0; i--) {
 
if (tag.charAt(i) == '|' && !nesting) {
 
last_attr = tag.substr(i+1);
 
tag = tag.substring(0, i);
 
break;
 
} else switch (tag.substr(i-1, 2)) {
 
case ']]':
 
nesting++;
 
i--;
 
break;
 
case '[[':
 
nesting--;
 
i--;
 
 
}
 
}
}
 
 
attr = tag.split(/\s*\|\s*/);
 
attr.push(last_attr);
 
filename = attr.shift();
 
 
var w_match;
 
 
for (;attr.length; attr.shift()) {
 
w_match = attr[0].match(/^(\d*)(?:[px]*\d*)?px$/);
 
if (w_match) width = w_match[1];
 
else switch(attr[0]) {
 
case 'thumb':
 
case 'thumbnail':
 
thumb=true;
 
frame=true;
 
break;
 
case 'frame':
 
frame=true;
 
break;
 
case 'none':
 
case 'right':
 
case 'left':
 
center=false;
 
align=attr[0];
 
break;
 
case 'center':
 
center=true;
 
align='none';
 
break;
 
default:
 
if (attr.length == 1) caption = attr[0];
 
}
 
 
}
 
}
  
} else filename = tag;
+
function parse_table_data() {
 +
var td_line, match_i;
  
 +
// 1: "|+", '|' or '+'
 +
// 2: ??
 +
// 3: attributes ??
 +
// TODO: finish commenting this regexp
 +
var td_match = sh().match(/^(\|\+|\||!)((?:([^[|]*?)\|(?!\|))?(.*))$/);
  
var o='';
+
if (td_match[1] == '|+') ps('<caption');
 +
else ps('<t' + (td_match[1] == '|' ? 'd' : 'h'));
  
if (frame) {
+
if (typeof td_match[3] != 'undefined') {
 +
//ps(' ' + td_match[3])
 +
match_i = 4;
 +
} else match_i = 2;
  
if (align === '') align = 'right';
+
ps('>');
  
o += f("<div class='thumb t?'>", align);
+
if (td_match[1] != '|+') {
 +
// use || or !! as a cell separator depending on context
 +
// NOTE: when split() is passed a regexp make sure to use non-capturing brackets
 +
td_line = td_match[match_i].split(td_match[1] == '|' ? '||' : /(?:\|\||!!)/);
  
if (thumb) {
+
ps(parse_inline_nowiki(td_line.shift()));
if (!width) width = Insta.conf.wiki.default_thumb_width;
 
  
o += f("<div style='width:?px;'>?", 2+width*1, make_image(filename, caption, width)) +
+
while (td_line.length) ll.unshift(td_match[1] + td_line.pop());
f("<div class='thumbcaption'><div class='magnify' style='float:right'><a href='?' title='Enlarge'></a></div>?</div>",
 
htmlescape_attr(Insta.conf.paths.articles + Insta.conf.locale.image + ':' + filename),
 
parse_inline_nowiki(caption)
 
);
 
 
} else {
 
} else {
o += '<div>' + make_image(filename, caption) + f("<div class='thumbcaption'>?</div>", parse_inline_nowiki(caption));
+
ps(parse_inline_nowiki(td_match[match_i]));
 
}
 
}
  
o += '</div></div>';
+
var tc = 0,
 +
td = [];
 +
 
 +
while (remain()) {
 +
td.push(sh());
 +
if (compareLineStringOrReg('|')) {
 +
if (!tc) break;
 +
// we're at the outer-most level (no nested tables), skip to td parse
 +
else if (charAtPoint(1) == '}') tc--;
 +
} else if (!tc && compareLineStringOrReg('!')) break;
 +
else if (compareLineStringOrReg('{|')) tc++;
 +
}
  
} else if (align !== '') {
+
if (td.length) ps(Insta.convert(td));
o += f("<div class='float?'><span>?</span></div>", align, make_image(filename, caption, width));
 
} else {
 
return make_image(filename, caption, width);
 
 
}
 
}
  
return center? f("<div class='center'>?</div>", o): o;
+
function parse_pre() {
//</NOLITE>
+
ps('<pre>');
}
 
 
 
function parse_inline_nowiki(str)
 
{
 
var start, lastend=0;
 
var substart=0, nestlev=0, open, close, subloop;
 
var html='';
 
 
 
while (-1 != (start = str.indexOf('<nowiki>', substart))) {
 
html += parse_inline_wiki(str.substring(lastend, start));
 
start += 8;
 
substart = start;
 
subloop = true;
 
 
do {
 
do {
open = str.indexOf('<nowiki>', substart);
+
endl(parse_inline_nowiki(ll[0].substring(1)) + '\n');
close = str.indexOf('</nowiki>', substart);
+
} while (remain() && compareLineStringOrReg(' '));
if (close<=open || open==-1) {
+
ps('</pre>');
if (close==-1) {
 
return html + html_entities(str.substr(start));
 
}
 
substart = close+9;
 
if (nestlev) {
 
nestlev--;
 
} else {
 
lastend = substart;
 
html += html_entities(str.substring(start, lastend-9));
 
subloop = false;
 
}
 
} else {
 
substart = open+8;
 
nestlev++;
 
}
 
} while (subloop);
 
 
}
 
}
  
return html + parse_inline_wiki(str.substr(lastend));
+
function parse_block_image() {
}
+
ps(parse_image(sh()));
 +
}
  
function make_image(filename, caption, width)
+
function parse_image(str) {
{
+
//<NOLITE>
//<NOLITE>
+
// get what's in between "[[Image:" and "]]"
// uppercase first letter in file name
+
var tag = str.substring(str.indexOf(':') + 1, str.length - 2);
filename = filename.charAt(0).toUpperCase() + filename.substr(1);
+
/* eslint-disable no-unused-vars */
// replace spaces with underscores
+
var width;
filename = filename.replace(/ /g, '_');
+
var attr = [],
 +
filename,
 +
caption = '';
 +
var thumb = 0,
 +
frame = 0,
 +
center = 0;
 +
var align = '';
 +
/* eslint-enable no-unused-vars */
  
caption = strip_inline_wiki(caption);
+
if (tag.match(/\|/)) {
 +
// manage nested links
 +
var nesting = 0;
 +
var last_attr;
 +
for (var i = tag.length - 1; i > 0; i--) {
 +
if (tag.charAt(i) == '|' && !nesting) {
 +
last_attr = tag.substr(i + 1);
 +
tag = tag.substring(0, i);
 +
break;
 +
} else
 +
switch (tag.substr(i - 1, 2)) {
 +
case ']]':
 +
nesting++;
 +
i--;
 +
break;
 +
case '[[':
 +
nesting--;
 +
i--;
 +
}
 +
}
  
var md5 = hex_md5(filename);
+
attr = tag.split(/\s*\|\s*/);
 +
attr.push(last_attr);
 +
filename = attr.shift();
  
var source = md5.charAt(0) + '/' + md5.substr(0,2) + '/' + filename;
+
var w_match;
  
if (width) width = "width='" + width + "px'";
+
for (; attr.length; attr.shift()) {
 +
w_match = attr[0].match(/^(\d*)(?:[px]*\d*)?px$/);
 +
if (w_match) width = w_match[1];
 +
else
 +
switch (attr[0]) {
 +
case 'thumb':
 +
case 'thumbnail':
 +
thumb = true;
 +
frame = true;
 +
break;
 +
case 'frame':
 +
frame = true;
 +
break;
 +
case 'none':
 +
case 'right':
 +
case 'left':
 +
center = false;
 +
align = attr[0];
 +
break;
 +
case 'center':
 +
center = true;
 +
align = 'none';
 +
break;
 +
default:
 +
if (attr.length == 1) caption = attr[0];
 +
}
 +
}
 +
} else filename = tag;
  
var img = "<img onerror=\""+
+
return '';
pg.escapeQuotesHTML("this.onerror=null;this.src='"+pg.jsescape(Insta.conf.paths.images_fallback + source)+"'")+
+
//</NOLITE>
"\" src=\""+pg.escapeQuotesHTML(Insta.conf.paths.images + source)+
+
}
"\" "+(caption!=='' ? "alt=\"" +pg.escapeQuotesHTML(caption) + "\"" : '')+
 
" "+width+">";
 
  
return f("<a class='image' ? href=\"?\">?</a>",
+
function parse_inline_nowiki(str) {
(caption!=='')? "title=\"" + pg.escapeQuotesHTML(caption) + "\"" : '',
+
var start,
pg.escapeQuotesHTML(Insta.conf.paths.articles + Insta.conf.locale.image + ':' + filename), img);
+
lastend = 0;
//</NOLITE>
+
var substart = 0,
}
+
nestlev = 0,
 
+
open,
function parse_inline_images(str)
+
close,
{
+
subloop;
//<NOLITE>
+
var html = '';
var start, substart=0, nestlev=0;
 
var loop, close, open, wiki, html;
 
  
while (-1 != (start=str.indexOf('[[', substart))) {
+
while (-1 != (start = str.indexOf('<nowiki>', substart))) {
if(str.substr(start+2).match(RegExp('^(Image|File|' + Insta.conf.locale.image + '):','i'))) {
+
html += parse_inline_wiki(str.substring(lastend, start));
loop=true;
+
start += 8;
substart=start;
+
substart = start;
 +
subloop = true;
 
do {
 
do {
substart+=2;
+
open = str.indexOf('<nowiki>', substart);
close=str.indexOf(']]',substart);
+
close = str.indexOf('</nowiki>', substart);
open=str.indexOf('[[',substart);
+
if (close <= open || open == -1) {
if (close<=open||open==-1) {
+
if (close == -1) {
if (close==-1) return str;
+
return html + html_entities(str.substr(start));
substart=close;
+
}
 +
substart = close + 9;
 
if (nestlev) {
 
if (nestlev) {
 
nestlev--;
 
nestlev--;
 
} else {
 
} else {
wiki=str.substring(start,close+2);
+
lastend = substart;
html=parse_image(wiki);
+
html += html_entities(str.substring(start, lastend - 9));
str=str.replace(wiki,html);
+
subloop = false;
substart=start+html.length;
 
loop=false;
 
 
}
 
}
 
} else {
 
} else {
substart=open;
+
substart = open + 8;
 
nestlev++;
 
nestlev++;
 
}
 
}
} while (loop);
+
} while (subloop);
 +
}
  
} else break;
+
return html + parse_inline_wiki(str.substr(lastend));
 
}
 
}
  
//</NOLITE>
+
function parse_inline_images(str) {
return str;
+
//<NOLITE>
}
+
var start,
 +
substart = 0,
 +
nestlev = 0;
 +
var loop, close, open, wiki, html;
  
// the output of this function doesn't respect the FILO structure of HTML
+
while (-1 != (start = str.indexOf('[[', substart))) {
// but since most browsers can handle it I'll save myself the hassle
+
if (
function parse_inline_formatting(str)
+
str.substr(start + 2).match(RegExp('^(Image|File|' + Insta.conf.locale.image + '):', 'i'))
{
+
) {
var em,st,i,li,o='';
+
loop = true;
while ((i=str.indexOf("''",li))+1) {
+
substart = start;
o += str.substring(li,i);
+
do {
li=i+2;
+
substart += 2;
if (str.charAt(i+2)=="'") {
+
close = str.indexOf(']]', substart);
li++;
+
open = str.indexOf('[[', substart);
st=!st;
+
if (close <= open || open == -1) {
o+=st?'<strong>':'</strong>';
+
if (close == -1) return str;
} else {
+
substart = close;
em=!em;
+
if (nestlev) {
o+=em?'<em>':'</em>';
+
nestlev--;
 +
} else {
 +
wiki = str.substring(start, close + 2);
 +
html = parse_image(wiki);
 +
str = str.replace(wiki, html);
 +
substart = start + html.length;
 +
loop = false;
 +
}
 +
} else {
 +
substart = open;
 +
nestlev++;
 +
}
 +
} while (loop);
 +
} else break;
 
}
 
}
 +
 +
//</NOLITE>
 +
return str;
 
}
 
}
return o+str.substr(li);
 
}
 
  
function parse_inline_wiki(str)
+
// the output of this function doesn't respect the FILO structure of HTML
{
+
// but since most browsers can handle it I'll save myself the hassle
var aux_match;
+
function parse_inline_formatting(str) {
 +
var em,
 +
st,
 +
i,
 +
li,
 +
o = '';
 +
while ((i = str.indexOf("''", li)) + 1) {
 +
o += str.substring(li, i);
 +
li = i + 2;
 +
if (str.charAt(i + 2) == "'") {
 +
li++;
 +
st = !st;
 +
o += st ? '<strong>' : '</strong>';
 +
} else {
 +
em = !em;
 +
o += em ? '<em>' : '</em>';
 +
}
 +
}
 +
return o + str.substr(li);
 +
}
  
str = parse_inline_images(str);
+
function parse_inline_wiki(str) {
str = parse_inline_formatting(str);
+
str = parse_inline_images(str);
 +
str = parse_inline_formatting(str);
  
// math
+
// math
str = str.replace(/<(?:)math>(.*?)<\/math>/ig, function(_,p1){
+
str = str.replace(/<(?:)math>(.*?)<\/math>/gi, '');
return f("<img src='?.png'>", Insta.conf.paths.math+hex_md5(p1));
 
});
 
  
// Build a Mediawiki-formatted date string
+
// Build a Mediawiki-formatted date string
var date = new Date();
+
var date = new Date();
var minutes = date.getUTCMinutes();
+
var minutes = date.getUTCMinutes();
if (minutes < 10) minutes = '0' + minutes;
+
if (minutes < 10) minutes = '0' + minutes;
date = f("?:?, ? ? ? (UTC)", date.getUTCHours(), minutes, date.getUTCDate(), Insta.conf.locale.months[date.getUTCMonth()], date.getUTCFullYear());
+
date = f(
 +
'?:?, ? ? ? (UTC)',
 +
date.getUTCHours(),
 +
minutes,
 +
date.getUTCDate(),
 +
Insta.conf.locale.months[date.getUTCMonth()],
 +
date.getUTCFullYear()
 +
);
  
// text formatting
+
// text formatting
return str.
+
return (
// signatures
+
str
replace(/~{5}(?!~)/g, date).
+
// signatures
replace(/~{4}(?!~)/g, Insta.conf.user.name+' '+date).
+
.replace(/~{5}(?!~)/g, date)
replace(/~{3}(?!~)/g, Insta.conf.user.name).
+
.replace(/~{4}(?!~)/g, Insta.conf.user.name + ' ' + date)
 
+
.replace(/~{3}(?!~)/g, Insta.conf.user.name)
// [[:Category:...]], [[:Image:...]], etc...
+
// [[:Category:...]], [[:Image:...]], etc...
replace(RegExp('\\[\\[:((?:'+Insta.conf.locale.category+'|Image|File|'+Insta.conf.locale.image+'|'+Insta.conf.wiki.interwiki+'):[^|]*?)\\]\\](\w*)','gi'), function($0,$1,$2){return f("<a href='?'>?</a>", Insta.conf.paths.articles + htmlescape_attr($1), htmlescape_text($1) + htmlescape_text($2));}).
+
.replace(
// remove straight category and interwiki tags
+
RegExp(
replace(RegExp('\\[\\[(?:'+Insta.conf.locale.category+'|'+Insta.conf.wiki.interwiki+'):.*?\\]\\]','gi'),'').
+
'\\[\\[:((?:' +
 
+
Insta.conf.locale.category +
// [[:Category:...|Links]], [[:Image:...|Links]], etc...
+
'|Image|File|' +
replace(RegExp('\\[\\[:((?:'+Insta.conf.locale.category+'|Image|File|'+Insta.conf.locale.image+'|'+Insta.conf.wiki.interwiki+'):.*?)\\|([^\\]]+?)\\]\\](\\w*)','gi'), function($0,$1,$2,$3){return f("<a href='?'>?</a>", Insta.conf.paths.articles + htmlescape_attr($1), htmlescape_text($2) + htmlescape_text($3));}).
+
Insta.conf.locale.image +
 +
'|' +
 +
Insta.conf.wiki.interwiki +
 +
'):[^|]*?)\\]\\](\\w*)',
 +
'gi'
 +
),
 +
function ($0, $1, $2) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.paths.articles + htmlescape_attr($1),
 +
htmlescape_text($1) + htmlescape_text($2)
 +
);
 +
}
 +
)
 +
// remove straight category and interwiki tags
 +
.replace(
 +
RegExp(
 +
'\\[\\[(?:' +
 +
Insta.conf.locale.category +
 +
'|' +
 +
Insta.conf.wiki.interwiki +
 +
'):.*?\\]\\]',
 +
'gi'
 +
),
 +
''
 +
)
 +
// [[:Category:...|Links]], [[:Image:...|Links]], etc...
 +
.replace(
 +
RegExp(
 +
'\\[\\[:((?:' +
 +
Insta.conf.locale.category +
 +
'|Image|File|' +
 +
Insta.conf.locale.image +
 +
'|' +
 +
Insta.conf.wiki.interwiki +
 +
'):.*?)\\|([^\\]]+?)\\]\\](\\w*)',
 +
'gi'
 +
),
 +
function ($0, $1, $2, $3) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.paths.articles + htmlescape_attr($1),
 +
htmlescape_text($2) + htmlescape_text($3)
 +
);
 +
}
 +
)
 +
// [[/Relative links]]
 +
.replace(/\[\[(\/[^|]*?)\]\]/g, function ($0, $1) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.baseUrl + htmlescape_attr($1),
 +
htmlescape_text($1)
 +
);
 +
})
 +
// [[/Replaced|Relative links]]
 +
.replace(/\[\[(\/.*?)\|(.+?)\]\]/g, function ($0, $1, $2) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.baseUrl + htmlescape_attr($1),
 +
htmlescape_text($2)
 +
);
 +
})
 +
// [[Common links]]
 +
.replace(/\[\[([^[|]*?)\]\](\w*)/g, function ($0, $1, $2) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.paths.articles + htmlescape_attr($1),
 +
htmlescape_text($1) + htmlescape_text($2)
 +
);
 +
})
 +
// [[Replaced|Links]]
 +
.replace(/\[\[([^[]*?)\|([^\]]+?)\]\](\w*)/g, function ($0, $1, $2, $3) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.paths.articles + htmlescape_attr($1),
 +
htmlescape_text($2) + htmlescape_text($3)
 +
);
 +
})
 +
// [[Stripped:Namespace|Namespace]]
 +
.replace(/\[\[([^\]]*?:)?(.*?)( *\(.*?\))?\|\]\]/g, function ($0, $1, $2, $3) {
 +
return f(
 +
"<a href='?'>?</a>",
 +
Insta.conf.paths.articles +
 +
htmlescape_attr($1) +
 +
htmlescape_attr($2) +
 +
htmlescape_attr($3),
 +
htmlescape_text($2)
 +
);
 +
})
 +
// External links
 +
.replace(
 +
/\[(https?|news|ftp|mailto|gopher|irc):(\/*)([^\]]*?) (.*?)\]/g,
 +
function ($0, $1, $2, $3, $4) {
 +
return f(
 +
"<a class='external' href='?:?'>?</a>",
 +
htmlescape_attr($1),
 +
htmlescape_attr($2) + htmlescape_attr($3),
 +
htmlescape_text($4)
 +
);
 +
}
 +
)
 +
.replace(/\[http:\/\/(.*?)\]/g, function ($0, $1) {
 +
return f("<a class='external' href='http://?'>[#]</a>", htmlescape_attr($1));
 +
})
 +
.replace(/\[(news|ftp|mailto|gopher|irc):(\/*)(.*?)\]/g, function ($0, $1, $2, $3) {
 +
return f(
 +
"<a class='external' href='?:?'>?:?</a>",
 +
htmlescape_attr($1),
 +
htmlescape_attr($2) + htmlescape_attr($3),
 +
htmlescape_text($1),
 +
htmlescape_text($2) + htmlescape_text($3)
 +
);
 +
})
 +
.replace(
 +
/(^| )(https?|news|ftp|mailto|gopher|irc):(\/*)([^ $]*[^.,!?;: $])/g,
 +
function ($0, $1, $2, $3, $4) {
 +
return f(
 +
"?<a class='external' href='?:?'>?:?</a>",
 +
htmlescape_text($1),
 +
htmlescape_attr($2),
 +
htmlescape_attr($3) + htmlescape_attr($4),
 +
htmlescape_text($2),
 +
htmlescape_text($3) + htmlescape_text($4)
 +
);
 +
}
 +
)
 +
.replace('__NOTOC__', '')
 +
.replace('__NOINDEX__', '')
 +
.replace('__INDEX__', '')
 +
.replace('__NOEDITSECTION__', '')
 +
);
 +
}
  
// [[/Relative links]]
+
// begin parsing
replace(/\[\[(\/[^|]*?)\]\]/g, function($0,$1){return f("<a href='?'>?</a>", Insta.conf.baseUrl + htmlescape_attr($1), htmlescape_text($1)); }).
+
for (; remain(); )
 +
if (compareLineStringOrReg(/^(={1,6})(.*)\1(.*)$/)) {
 +
p = 0;
 +
endl(f('<h?>?</h?>?', r[1].length, parse_inline_nowiki(r[2]), r[1].length, r[3]));
 +
} else if (compareLineStringOrReg(/^[*#:;]/)) {
 +
p = 0;
 +
parse_list();
 +
} else if (compareLineStringOrReg(' ')) {
 +
p = 0;
 +
parse_pre();
 +
} else if (compareLineStringOrReg('{|')) {
 +
p = 0;
 +
parse_table();
 +
} else if (compareLineStringOrReg(/^----+$/)) {
 +
p = 0;
 +
endl('<hr />');
 +
} else if (compareLineStringOrReg(Insta.BLOCK_IMAGE)) {
 +
p = 0;
 +
parse_block_image();
 +
} else {
 +
// handle paragraphs
 +
if (compareLineString('')) {
 +
p = remain() > 1 && ll[1] === '';
 +
if (p) endl('<p><br>');
 +
} else {
 +
if (!p) {
 +
ps('<p>');
 +
p = 1;
 +
}
 +
ps(parse_inline_nowiki(ll[0]) + ' ');
 +
}
  
// [[/Replaced|Relative links]]
+
sh();
replace(/\[\[(\/.*?)\|(.+?)\]\]/g, function($0,$1,$2){return f("<a href='?'>?</a>", Insta.conf.baseUrl + htmlescape_attr($1), htmlescape_text($2)); }).
+
}
  
// [[Common links]]
+
return o;
replace(/\[\[([^[|]*?)\]\](\w*)/g, function($0,$1,$2){return f("<a href='?'>?</a>", Insta.conf.paths.articles + htmlescape_attr($1), htmlescape_text($1) + htmlescape_text($2)); }).
+
};
  
// [[Replaced|Links]]
+
function wiki2html(txt, baseurl) {
replace(/\[\[([^[]*?)\|([^\]]+?)\]\](\w*)/g, function($0,$1,$2,$3){return f("<a href='?'>?</a>", Insta.conf.paths.articles + htmlescape_attr($1), htmlescape_text($2) + htmlescape_text($3)); }).
+
Insta.conf.baseUrl = baseurl;
 +
return Insta.convert(txt);
 +
}
 +
// ENDFILE: livepreview.js
  
// [[Stripped:Namespace|Namespace]]
+
// STARTFILE: pageinfo.js
replace(/\[\[([^\]]*?:)?(.*?)( *\(.*?\))?\|\]\]/g, function($0,$1,$2,$3){return f("<a href='?'>?</a>", Insta.conf.paths.articles + htmlescape_attr($1) + htmlescape_attr($2) + htmlescape_attr($3), htmlescape_text($2)); }).
+
//<NOLITE>
 +
function popupFilterPageSize(data) {
 +
return formatBytes(data.length);
 +
}
  
// External links
+
function popupFilterCountLinks(data) {
replace(/\[(https?|news|ftp|mailto|gopher|irc):(\/*)([^\]]*?) (.*?)\]/g, function($0,$1,$2,$3,$4){return f("<a class='external' href='?:?'>?</a>", htmlescape_attr($1), htmlescape_attr($2) + htmlescape_attr($3), htmlescape_text($4)); }).
+
var num = countLinks(data);
replace(/\[http:\/\/(.*?)\]/g, function($0,$1){return f("<a class='external' href='http://?'>[#]</a>", htmlescape_attr($1)); }).
+
return String(num) + '&nbsp;' + (num != 1 ? popupString('wikiLinks') : popupString('wikiLink'));
replace(/\[(news|ftp|mailto|gopher|irc):(\/*)(.*?)\]/g, function($0,$1,$2,$3,$4){return f("<a class='external' href='?:?'>?:?</a>", htmlescape_attr($1), htmlescape_attr($2) + htmlescape_attr($3), htmlescape_text($1), htmlescape_text($2) + htmlescape_text($3)); }).
+
}
replace(/(^| )(https?|news|ftp|mailto|gopher|irc):(\/*)([^ $]*[^.,!?;: $])/g, function($0,$1,$2,$3,$4){return f("?<a class='external' href='?:?'>?:?</a>", htmlescape_text($1), htmlescape_attr($2), htmlescape_attr($3) + htmlescape_attr($4), htmlescape_text($2), htmlescape_text($3) + htmlescape_text($4)); }).
 
  
replace('__NOTOC__','').
+
function popupFilterCountImages(data) {
replace('__NOEDITSECTION__','');
+
var num = countImages(data);
 +
return String(num) + '&nbsp;' + (num != 1 ? popupString('images') : popupString('image'));
 
}
 
}
/*
+
 
*/
+
function popupFilterCountCategories(data) {
function strip_inline_wiki(str)
+
var num = countCategories(data);
{
+
return (
return str
+
String(num) + '&nbsp;' + (num != 1 ? popupString('categories') : popupString('category'))
.replace(/\[\[[^\]]*\|(.*?)\]\]/g,'$1')
+
);
.replace(/\[\[(.*?)\]\]/g,'$1')
 
.replace(/''(.*?)''/g,'$1');
 
 
}
 
}
  
// begin parsing
+
function popupFilterLastModified(data, download) {
for (;remain();) if ($(/^(={1,6})(.*)\1(.*)$/)) {
+
var lastmod = download.lastModified;
p=0;
+
var now = new Date();
endl(f('<h?>?</h?>?', $r[1].length, parse_inline_nowiki($r[2]), $r[1].length, $r[3]));
+
var age = now - lastmod;
 +
if (lastmod && getValueOf('popupLastModified')) {
 +
return tprintf('%s old', [formatAge(age)]).replace(RegExp(' ', 'g'), '&nbsp;');
 +
}
 +
return '';
 +
}
  
} else if ($(/^[*#:;]/)) {
+
function formatAge(age) {
p=0;
+
// coerce into a number
parse_list();
+
var a = 0 + age,
 +
aa = a;
  
} else if ($(' ')) {
+
var seclen = 1000;
p=0;
+
var minlen = 60 * seclen;
parse_pre();
+
var hourlen = 60 * minlen;
 +
var daylen = 24 * hourlen;
 +
var weeklen = 7 * daylen;
  
} else if ($('{|')) {
+
var numweeks = (a - (a % weeklen)) / weeklen;
p=0;
+
a = a - numweeks * weeklen;
parse_table();
+
var sweeks = addunit(numweeks, 'week');
 +
var numdays = (a - (a % daylen)) / daylen;
 +
a = a - numdays * daylen;
 +
var sdays = addunit(numdays, 'day');
 +
var numhours = (a - (a % hourlen)) / hourlen;
 +
a = a - numhours * hourlen;
 +
var shours = addunit(numhours, 'hour');
 +
var nummins = (a - (a % minlen)) / minlen;
 +
a = a - nummins * minlen;
 +
var smins = addunit(nummins, 'minute');
 +
var numsecs = (a - (a % seclen)) / seclen;
 +
a = a - numsecs * seclen;
 +
var ssecs = addunit(numsecs, 'second');
  
} else if ($(/^----+$/)) {
+
if (aa > 4 * weeklen) {
p=0;
+
return sweeks;
endl('<hr />');
+
}
 +
if (aa > weeklen) {
 +
return sweeks + ' ' + sdays;
 +
}
 +
if (aa > daylen) {
 +
return sdays + ' ' + shours;
 +
}
 +
if (aa > 6 * hourlen) {
 +
return shours;
 +
}
 +
if (aa > hourlen) {
 +
return shours + ' ' + smins;
 +
}
 +
if (aa > 10 * minlen) {
 +
return smins;
 +
}
 +
if (aa > minlen) {
 +
return smins + ' ' + ssecs;
 +
}
 +
return ssecs;
 +
}
  
} else if ($(Insta.BLOCK_IMAGE)) {
+
function addunit(num, str) {
p=0;
+
return '' + num + ' ' + (num != 1 ? popupString(str + 's') : popupString(str));
parse_block_image();
+
}
  
} else {
+
function runPopupFilters(list, data, download) {
 
+
var ret = [];
// handle paragraphs
+
for (var i = 0; i < list.length; ++i) {
if ($$('')) {
+
if (list[i] && typeof list[i] == 'function') {
p = (remain()>1 && ll[1]===(''));
+
var s = list[i](data, download, download.owner.article);
if (p) endl('<p><br>');
+
if (s) {
} else {
+
ret.push(s);
if(!p) {
+
}
ps('<p>');
 
p=1;
 
 
}
 
}
ps(parse_inline_nowiki(ll[0]) + ' ');
 
 
}
 
}
 
+
return ret;
sh();
 
 
}
 
}
  
return o;
+
function getPageInfo(data, download) {
};
+
if (!data || data.length === 0) {
 +
return popupString('Empty page');
 +
}
  
function wiki2html(txt,baseurl) {
+
var popupFilters = getValueOf('popupFilters') || [];
Insta.conf.baseUrl=baseurl;
+
var extraPopupFilters = getValueOf('extraPopupFilters') || [];
return Insta.convert(txt);
+
var pageInfoArray = runPopupFilters(popupFilters.concat(extraPopupFilters), data, download);
}
 
// ENDFILE: livepreview.js
 
// STARTFILE: pageinfo.js
 
//<NOLITE>
 
function popupFilterPageSize(data) {
 
return formatBytes(data.length);
 
}
 
  
function popupFilterCountLinks(data) {
+
var pageInfo = pageInfoArray.join(', ');
var num=countLinks(data);
+
if (pageInfo !== '') {
return String(num) + '&nbsp;' + ((num!=1)?popupString('wikiLinks'):popupString('wikiLink'));
+
pageInfo = upcaseFirst(pageInfo);
}
+
}
 +
return pageInfo;
 +
}
  
function popupFilterCountImages(data) {
+
// this could be improved!
var num=countImages(data);
+
function countLinks(wikiText) {
return String(num) + '&nbsp;' + ((num!=1)?popupString('images'):popupString('image'));
+
return wikiText.split('[[').length - 1;
}
+
}
  
function popupFilterCountCategories(data) {
+
// if N = # matches, n = # brackets, then
var num=countCategories(data);
+
// String.parenSplit(regex) intersperses the N+1 split elements
return String(num) + '&nbsp;' + ((num!=1)?popupString('categories'):popupString('category'));
+
// with Nn other elements. So total length is
}
+
// L= N+1 + Nn = N(n+1)+1. So N=(L-1)/(n+1).
  
 +
function countImages(wikiText) {
 +
return (wikiText.parenSplit(pg.re.image).length - 1) / (pg.re.imageBracketCount + 1);
 +
}
  
function popupFilterLastModified(data,download) {
+
function countCategories(wikiText) {
var lastmod=download.lastModified;
+
return (wikiText.parenSplit(pg.re.category).length - 1) / (pg.re.categoryBracketCount + 1);
var now=new Date();
 
var age=now-lastmod;
 
if (lastmod && getValueOf('popupLastModified')) {
 
return (tprintf('%s old', [formatAge(age)])).replace(RegExp(' ','g'), '&nbsp;');
 
 
}
 
}
return '';
 
}
 
  
function formatAge(age) {
+
function popupFilterStubDetect(data, download, article) {
// coerce into a number
+
var counts = stubCount(data, article);
var a=0+age, aa=a;
+
if (counts.real) {
 
+
return popupString('stub');
var seclen  = 1000;
+
}
var minlen  = 60*seclen;
+
if (counts.sect) {
var hourlen = 60*minlen;
+
return popupString('section stub');
var daylen  = 24*hourlen;
+
}
var weeklen = 7*daylen;
+
return '';
 
+
}
var numweeks = (a-a%weeklen)/weeklen; a = a-numweeks*weeklen; var sweeks = addunit(numweeks, 'week');
 
var numdays  = (a-a%daylen)/daylen;  a = a-numdays*daylen;  var sdays  = addunit(numdays, 'day');
 
var numhours = (a-a%hourlen)/hourlen; a = a-numhours*hourlen; var shours = addunit(numhours,'hour');
 
var nummins  = (a-a%minlen)/minlen;  a = a-nummins*minlen;  var smins  = addunit(nummins, 'minute');
 
var numsecs  = (a-a%seclen)/seclen;  a = a-numsecs*seclen;  var ssecs  = addunit(numsecs, 'second');
 
  
if (aa > 4*weeklen) { return sweeks; }
+
function popupFilterDisambigDetect(data, download, article) {
if (aa > weeklen)  { return sweeks + ' ' + sdays; }
+
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
if (aa > daylen) { return sdays  + ' ' + shours; }
+
return '';
if (aa > 6*hourlen) { return shours; }
 
if (aa > hourlen)   { return shours + ' ' + smins; }
 
if (aa > 10*minlen) { return smins; }
 
if (aa > minlen) { return smins  + ' ' + ssecs; }
 
return ssecs;
 
}
 
 
 
function addunit(num,str) { return '' + num + ' ' + ((num!=1) ? popupString(str+'s') : popupString(str)) ;}
 
 
 
function runPopupFilters(list, data, download) {
 
var ret=[];
 
for (var i=0; i<list.length; ++i) {
 
if (list[i] && typeof list[i] == 'function') {
 
var s=list[i](data, download, download.owner.article);
 
if (s) { ret.push(s); }
 
 
}
 
}
 +
return isDisambig(data, article) ? popupString('disambig') : '';
 
}
 
}
return ret;
 
}
 
  
function getPageInfo(data, download) {
+
function formatBytes(num) {
if (!data || data.length === 0) { return popupString('Empty page'); }
+
return num > 949
 +
? Math.round(num / 100) / 10 + popupString('kB')
 +
: num + '&nbsp;' + popupString('bytes');
 +
}
 +
//</NOLITE>
 +
// ENDFILE: pageinfo.js
  
var popupFilters=getValueOf('popupFilters') || [];
+
// STARTFILE: titles.js
var extraPopupFilters = getValueOf('extraPopupFilters') || [];
+
/**
var pageInfoArray = runPopupFilters(popupFilters.concat(extraPopupFilters), data, download);
+
* @fileoverview Defines the {@link Title} class, and associated crufty functions.
  
var pageInfo=pageInfoArray.join(', ');
+
* <code>Title</code> deals with article titles and their various
if (pageInfo !== '' ) { pageInfo = upcaseFirst(pageInfo); }
+
* forms. {@link Stringwrapper} is the parent class of
return pageInfo;
+
* <code>Title</code>, which exists simply to make things a little
}
+
* neater.
 +
*/
  
 +
/**
 +
* Creates a new Stringwrapper.
 +
* @constructor
  
// this could be improved!
+
* @class the Stringwrapper class. This base class is not really
function countLinks(wikiText) { return wikiText.split('[[').length - 1; }
+
* useful on its own; it just wraps various common string operations.
 +
*/
 +
function Stringwrapper() {
 +
/**
 +
* Wrapper for this.toString().indexOf()
 +
* @param {String} x
 +
* @type integer
 +
*/
 +
this.indexOf = function (x) {
 +
return this.toString().indexOf(x);
 +
};
 +
/**
 +
* Returns this.value.
 +
* @type String
 +
*/
 +
this.toString = function () {
 +
return this.value;
 +
};
 +
/**
 +
* Wrapper for {@link String#parenSplit} applied to this.toString()
 +
* @param {RegExp} x
 +
* @type Array
 +
*/
 +
this.parenSplit = function (x) {
 +
return this.toString().parenSplit(x);
 +
};
 +
/**
 +
* Wrapper for this.toString().substring()
 +
* @param {String} x
 +
* @param {String} y (optional)
 +
* @type String
 +
*/
 +
this.substring = function (x, y) {
 +
if (typeof y == 'undefined') {
 +
return this.toString().substring(x);
 +
}
 +
return this.toString().substring(x, y);
 +
};
 +
/**
 +
* Wrapper for this.toString().split()
 +
* @param {String} x
 +
* @type Array
 +
*/
 +
this.split = function (x) {
 +
return this.toString().split(x);
 +
};
 +
/**
 +
* Wrapper for this.toString().replace()
 +
* @param {String} x
 +
* @param {String} y
 +
* @type String
 +
*/
 +
this.replace = function (x, y) {
 +
return this.toString().replace(x, y);
 +
};
 +
}
  
// if N = # matches, n = # brackets, then
+
/**
// String.parenSplit(regex) intersperses the N+1 split elements
+
* Creates a new <code>Title</code>.
// with Nn other elements. So total length is
+
* @constructor
// L= N+1 + Nn = N(n+1)+1. So N=(L-1)/(n+1).
+
*
 +
* @class The Title class. Holds article titles and converts them into
 +
* various forms. Also deals with anchors, by which we mean the bits
 +
* of the article URL after a # character, representing locations
 +
* within an article.
 +
*
 +
* @param {String} value The initial value to assign to the
 +
* article. This must be the canonical title (see {@link
 +
* Title#value}. Omit this in the constructor and use another function
 +
* to set the title if this is unavailable.
 +
*/
 +
function Title(val) {
 +
/**
 +
* The canonical article title. This must be in UTF-8 with no
 +
* entities, escaping or nasties. Also, underscores should be
 +
* replaced with spaces.
 +
* @type String
 +
* @private
 +
*/
 +
this.value = null;
  
function countImages(wikiText) {
+
/**
return (wikiText.parenSplit(pg.re.image).length - 1) / (pg.re.imageBracketCount + 1);
+
* The canonical form of the anchor. This should be exactly as
}
+
* it appears in the URL, i.e. with the .C3.0A bits in.
 +
* @type String
 +
*/
 +
this.anchor = '';
  
function countCategories(wikiText) {
+
this.setUtf(val);
return (wikiText.parenSplit(pg.re.category).length - 1) / (pg.re.categoryBracketCount + 1);
+
}
}
+
Title.prototype = new Stringwrapper();
 
+
/**
function popupFilterStubDetect(data, download, article) {
+
* Returns the canonical representation of the article title, optionally without anchor.
var counts=stubCount(data, article);
+
* @param {boolean} omitAnchor
if (counts.real) { return popupString('stub'); }
+
* @fixme Decide specs for anchor
if (counts.sect) { return popupString('section stub'); }
+
* @return String The article title and the anchor.
return '';
+
*/
}
+
Title.prototype.toString = function (omitAnchor) {
 
+
return this.value + (!omitAnchor && this.anchor ? '#' + this.anchorString() : '');
function popupFilterDisambigDetect(data, download, article) {
+
};
if (getValueOf('popupOnlyArticleDabStub') && article.namespace()) { return ''; }
+
Title.prototype.anchorString = function () {
return (isDisambig(data, article)) ? popupString('disambig') : '';
+
if (!this.anchor) {
}
+
return '';
 +
}
 +
var split = this.anchor.parenSplit(/((?:[.][0-9A-F]{2})+)/);
 +
var len = split.length;
 +
var value;
 +
for (var j = 1; j < len; j += 2) {
 +
// FIXME s/decodeURI/decodeURIComponent/g ?
 +
value = split[j].split('.').join('%');
 +
try {
 +
value = decodeURIComponent(value);
 +
} catch (e) {
 +
// cannot decode
 +
}
 +
split[j] = value.split('_').join(' ');
 +
}
 +
return split.join('');
 +
};
 +
Title.prototype.urlAnchor = function () {
 +
var split = this.anchor.parenSplit('/((?:[%][0-9A-F]{2})+)/');
 +
var len = split.length;
 +
for (var j = 1; j < len; j += 2) {
 +
split[j] = split[j].split('%').join('.');
 +
}
 +
return split.join('');
 +
};
 +
Title.prototype.anchorFromUtf = function (str) {
 +
this.anchor = encodeURIComponent(str.split(' ').join('_'))
 +
.split('%3A')
 +
.join(':')
 +
.split("'")
 +
.join('%27')
 +
.split('%')
 +
.join('.');
 +
};
 +
Title.fromURL = function (h) {
 +
return new Title().fromURL(h);
 +
};
 +
Title.prototype.fromURL = function (h) {
 +
if (typeof h != 'string') {
 +
this.value = null;
 +
return this;
 +
}
  
function formatBytes(num) {
+
// NOTE : playing with decodeURI, encodeURI, escape, unescape,
return (num > 949) ? (Math.round(num/100)/10+popupString('kB')) : (num +'&nbsp;' + popupString('bytes')) ;
+
// we seem to be able to replicate the IE borked encoding
}
 
//</NOLITE>
 
// ENDFILE: pageinfo.js
 
// STARTFILE: titles.js
 
/**
 
  @fileoverview Defines the {@link Title} class, and associated crufty functions.
 
  
  <code>Title</code> deals with article titles and their various
+
// IE doesn't do this new-fangled utf-8 thing.
  forms.  {@link Stringwrapper} is the parent class of
+
// and it's worse than that.
  <code>Title</code>, which exists simply to make things a little
+
// IE seems to treat the query string differently to the rest of the url
  neater.
+
// the query is treated as bona-fide utf8, but the first bit of the url is pissed around with
  
*/
+
// we fix up & for all browsers, just in case.
 +
var splitted = h.split('?');
 +
splitted[0] = splitted[0].split('&').join('%26');
  
/**
+
h = splitted.join('?');
  Creates a new Stringwrapper.
 
  @constructor
 
  
  @class the Stringwrapper class. This base class is not really
+
var contribs = pg.re.contribs.exec(h);
  useful on its own; it just wraps various common string operations.
+
if (contribs) {
*/
+
if (contribs[1] == 'title=') {
function Stringwrapper() {
+
contribs[3] = contribs[3].split('+').join(' ');
/**
+
}
  Wrapper for this.toString().indexOf()
+
var u = new Title(contribs[3]);
  @param {String} x
+
this.setUtf(
  @type integer
+
this.decodeNasties(
*/
+
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + u.stripNamespace()
this.indexOf=function(x){return this.toString().indexOf(x);};
+
)
/**
+
);
  Returns this.value.
+
return this;
  @type String
+
}
*/
 
this.toString=function(){return this.value;};
 
/**
 
  Wrapper for {@link String#parenSplit} applied to this.toString()
 
  @param {RegExp} x
 
  @type Array
 
*/
 
this.parenSplit=function(x){return this.toString().parenSplit(x);};
 
/**
 
  Wrapper for this.toString().substring()
 
  @param {String} x
 
  @param {String} y (optional)
 
  @type String
 
*/
 
this.substring=function(x,y){
 
if (typeof y=='undefined') { return this.toString().substring(x); }
 
return this.toString().substring(x,y);
 
};
 
/**
 
  Wrapper for this.toString().split()
 
  @param {String} x
 
  @type Array
 
*/
 
this.split=function(x){return this.toString().split(x);};
 
/**
 
  Wrapper for this.toString().replace()
 
  @param {String} x
 
  @param {String} y
 
  @type String
 
*/
 
this.replace=function(x,y){ return this.toString().replace(x,y); };
 
}
 
  
 +
var email = pg.re.email.exec(h);
 +
if (email) {
 +
this.setUtf(
 +
this.decodeNasties(
 +
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] +
 +
':' +
 +
new Title(email[3]).stripNamespace()
 +
)
 +
);
 +
return this;
 +
}
  
/**
+
var backlinks = pg.re.backlinks.exec(h);
  Creates a new <code>Title</code>.
+
if (backlinks) {
  @constructor
+
this.setUtf(this.decodeNasties(new Title(backlinks[3])));
 +
return this;
 +
}
  
  @class The Title class. Holds article titles and converts them into
+
//A dummy title object for a Special:Diff link.
  various forms. Also deals with anchors, by which we mean the bits
+
var specialdiff = pg.re.specialdiff.exec(h);
  of the article URL after a # character, representing locations
+
if (specialdiff) {
  within an article.
+
this.setUtf(
 +
this.decodeNasties(
 +
new Title(mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':Diff')
 +
)
 +
);
 +
return this;
 +
}
  
  @param {String} value The initial value to assign to the
+
// no more special cases to check --
  article. This must be the canonical title (see {@link
+
// hopefully it's not a disguised user-related or specially treated special page
  Title#value}. Omit this in the constructor and use another function
+
// Includes references
  to set the title if this is unavailable.
+
var m = pg.re.main.exec(h);
*/
+
if (m === null) {
function Title(val) {
+
this.value = null;
/**
+
} else {
  The canonical article title. This must be in UTF-8 with no
+
var fromBotInterface = /[?](.+[&])?title=/.test(h);
  entities, escaping or nasties. Also, underscores should be
+
if (fromBotInterface) {
  replaced with spaces.
+
m[2] = m[2].split('+').join('_');
  @type String
+
}
  @private
+
var extracted = m[2] + (m[3] ? '#' + m[3] : '');
*/
+
if (pg.flag.isSafari && /%25[0-9A-Fa-f]{2}/.test(extracted)) {
this.value=null;
+
// Fix Safari issue
/**
+
// Safari sometimes encodes % as %25 in UTF-8 encoded strings like %E5%A3 -> %25E5%25A3.
  The canonical form of the anchor. This should be exactly as
+
this.setUtf(decodeURIComponent(unescape(extracted)));
  it appears in the URL, i.e. with the .C3.0A bits in.
+
} else {
  @type String
+
this.setUtf(this.decodeNasties(extracted));
*/
+
}
this.anchor='';
+
}
 
 
this.setUtf(val);
 
}
 
Title.prototype=new Stringwrapper();
 
/**
 
  Returns the canonical representation of the article title, optionally without anchor.
 
  @param {boolean} omitAnchor
 
  @fixme Decide specs for anchor
 
  @return String The article title and the anchor.
 
*/
 
Title.prototype.toString=function(omitAnchor) {
 
return this.value + ( (!omitAnchor && this.anchor) ? '#' + this.anchorString() : '' );
 
};
 
Title.prototype.anchorString=function() {
 
if (!this.anchor) { return ''; }
 
var split=this.anchor.parenSplit(/((?:[.][0-9A-F]{2})+)/);
 
var len=split.length;
 
for (var j=1; j<len; j+=2) {
 
// FIXME s/decodeURI/decodeURIComponent/g ?
 
split[j]=decodeURIComponent(split[j].split('.').join('%')).split('_').join(' ');
 
}
 
return split.join('');
 
};
 
Title.prototype.urlAnchor=function() {
 
var split=this.anchor.parenSplit('/((?:[%][0-9A-F]{2})+)/');
 
var len=split.length;
 
for (var j=1; j<len; j+=2) {
 
split[j]=split[j].split('%').join('.');
 
}
 
return split.join('');
 
};
 
Title.prototype.anchorFromUtf=function(str) {
 
this.anchor=encodeURIComponent(str.split(' ').join('_'))
 
.split('%3A').join(':').split("'").join('%27').split('%').join('.');
 
};
 
Title.fromURL=function(h) {
 
return new Title().fromURL(h);
 
};
 
Title.prototype.fromURL=function(h) {
 
if (typeof h != 'string') {
 
this.value=null;
 
 
return this;
 
return this;
}
+
};
 
+
Title.prototype.decodeNasties = function (txt) {
// NOTE : playing with decodeURI, encodeURI, escape, unescape,
+
// myDecodeURI uses decodeExtras, which removes _,
// we seem to be able to replicate the IE borked encoding
+
// thus ruining citations previews, which are formated as "cite_note-1"
 
+
try {
// IE doesn't do this new-fangled utf-8 thing.
+
var ret = decodeURI(this.decodeEscapes(txt));
// and it's worse than that.
+
ret = ret.replace(/[_ ]*$/, '');
// IE seems to treat the query string differently to the rest of the url
+
return ret;
// the query is treated as bona-fide utf8, but the first bit of the url is pissed around with
+
} catch (e) {
 
+
return txt; // cannot decode
// we fix up & for all browsers, just in case.
+
}
var splitted=h.split('?');
+
};
splitted[0]=splitted[0].split('&').join('%26');
+
// Decode valid %-encodings, otherwise escape them
 
+
Title.prototype.decodeEscapes = function (txt) {
if (pg.flag.linksLikeIE6) {
+
var split = txt.parenSplit(/((?:[%][0-9A-Fa-f]{2})+)/);
splitted[0]=encodeURI(decode_utf8(splitted[0]));
+
var len = split.length;
}
+
// No %-encoded items found, so replace the literal %
 
+
if (len === 1) {
h=splitted.join('?');
+
return split[0].replace(/%(?![0-9a-fA-F][0-9a-fA-F])/g, '%25');
 
+
}
var contribs=pg.re.contribs.exec(h);
+
for (var i = 1; i < len; i = i + 2) {
if (contribs) {
+
split[i] = decodeURIComponent(split[i]);
if (contribs[1]=='title=') { contribs[3]=contribs[3].split('+').join(' '); }
+
}
var u=new Title(contribs[3]);
+
return split.join('');
this.setUtf(this.decodeNasties(mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + u.stripNamespace()));
+
};
 +
Title.fromAnchor = function (a) {
 +
return new Title().fromAnchor(a);
 +
};
 +
Title.prototype.fromAnchor = function (a) {
 +
if (!a) {
 +
this.value = null;
 +
return this;
 +
}
 +
return this.fromURL(a.href);
 +
};
 +
Title.fromWikiText = function (txt) {
 +
return new Title().fromWikiText(txt);
 +
};
 +
Title.prototype.fromWikiText = function (txt) {
 +
// FIXME - testing needed
 +
txt = myDecodeURI(txt);
 +
this.setUtf(txt);
 
return this;
 
return this;
}
+
};
 
+
Title.prototype.hintValue = function () {
var email=pg.re.email.exec(h);
+
if (!this.value) {
if (email) {
+
return '';
this.setUtf(this.decodeNasties(mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + new Title(email[3]).stripNamespace()));
+
}
return this;
+
return safeDecodeURI(this.value);
}
+
};
 
+
//<NOLITE>
var backlinks=pg.re.backlinks.exec(h);
+
Title.prototype.toUserName = function (withNs) {
if (backlinks) {
+
if (this.namespaceId() != pg.nsUserId && this.namespaceId() != pg.nsUsertalkId) {
this.setUtf(this.decodeNasties(new Title(backlinks[3])));
+
this.value = null;
return this;
+
return;
}
 
 
 
//A dummy title object for a Special:Diff link.
 
var specialdiff=pg.re.specialdiff.exec(h);
 
if (specialdiff) {
 
this.setUtf(this.decodeNasties(new Title(mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':Diff')));
 
return this;
 
}
 
 
 
// no more special cases to check --
 
// hopefully it's not a disguised user-related or specially treated special page
 
var m=pg.re.main.exec(h);
 
if(m === null) { this.value=null; }
 
else {
 
var fromBotInterface = /[?](.+[&])?title=/.test(h);
 
if (fromBotInterface) {
 
m[2]=m[2].split('+').join('_');
 
 
}
 
}
var extracted = m[2] + (m[3] ? '#' + m[3] : '');
+
this.value =
if (pg.flag.isSafari && /%25[0-9A-Fa-f]{2}/.test(extracted)) {
+
(withNs ? mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' : '') +
// Fix Safari issue
+
this.stripNamespace().split('/')[0];
// Safari sometimes encodes % as %25 in UTF-8 encoded strings like %E5%A3 -> %25E5%25A3.
+
};
this.setUtf(decodeURIComponent(unescape(extracted)));
+
Title.prototype.userName = function (withNs) {
} else {
+
var t = new Title(this.value);
this.setUtf(this.decodeNasties(extracted));
+
t.toUserName(withNs);
 +
if (t.value) {
 +
return t;
 
}
 
}
}
+
return null;
return this;
+
};
};
+
Title.prototype.toTalkPage = function () {
Title.prototype.decodeNasties=function(txt) {
+
// convert article to a talk page, or if we can't, return null
var ret= this.decodeEscapes(decodeURI(txt));
+
// In other words: return null if this ALREADY IS a talk page
ret = ret.replace(/[_ ]*$/, '');
+
// and return the corresponding talk page otherwise
return ret;
+
//
};
+
// Per https://www.mediawiki.org/wiki/Manual:Namespace#Subject_and_talk_namespaces
Title.prototype.decodeEscapes=function(txt) {
+
// * All discussion namespaces have odd-integer indices
var split=txt.parenSplit(/((?:[%][0-9A-Fa-f]{2})+)/);
+
// * The discussion namespace index for a specific namespace with index n is n + 1
var len=split.length;
+
if (this.value === null) {
for (var i=1; i<len; i=i+2) {
+
return null;
// FIXME is decodeURIComponent better?
 
split[i]=unescape(split[i]);
 
}
 
return split.join('');
 
};
 
Title.fromAnchor=function(a) {
 
return new Title().fromAnchor(a);
 
};
 
Title.prototype.fromAnchor=function(a) {
 
if (!a) { this.value=null; return this; }
 
return this.fromURL(a.href);
 
};
 
Title.fromWikiText=function(txt) {
 
return new Title().fromWikiText(txt);
 
};
 
Title.prototype.fromWikiText=function(txt) {
 
// FIXME - testing needed
 
if (!pg.flag.linksLikeIE6) { txt=myDecodeURI(txt); }
 
this.setUtf(txt);
 
return this;
 
};
 
Title.prototype.hintValue=function(){
 
if(!this.value) { return ''; }
 
return safeDecodeURI(this.value);
 
};
 
//<NOLITE>
 
Title.prototype.toUserName=function(withNs) {
 
if (this.namespaceId() != pg.nsUserId && this.namespaceId() != pg.nsUsertalkId) {
 
this.value=null;
 
return;
 
}
 
this.value = (withNs ? mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' : '') + this.stripNamespace().split('/')[0];
 
};
 
Title.prototype.userName=function(withNs) {
 
var t=(new Title(this.value));
 
t.toUserName(withNs);
 
if (t.value) { return t; }
 
return null;
 
};
 
Title.prototype.toTalkPage=function() {
 
// convert article to a talk page, or if we can't, return null
 
// In other words: return null if this ALREADY IS a talk page
 
// and return the corresponding talk page otherwise
 
//
 
// Per http://www.mediawiki.org/wiki/Manual:Namespace#Subject_and_talk_namespaces
 
// * All discussion namespaces have odd-integer indices
 
// * The discussion namespace index for a specific namespace with index n is n + 1
 
if (this.value === null) { return null; }
 
 
var namespaceId = this.namespaceId();
 
if (namespaceId>=0 && namespaceId % 2 === 0) //non-special and subject namespace
 
{
 
var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId+1];
 
if (typeof localizedNamespace!=='undefined')
 
{
 
if (localizedNamespace === '') {
 
this.value = this.stripNamespace();
 
} else {
 
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
 
}
 
return this.value;
 
 
}
 
}
}
 
  
this.value=null;
+
var namespaceId = this.namespaceId();
return null;
+
if (namespaceId >= 0 && namespaceId % 2 === 0) {
};
+
//non-special and subject namespace
//</NOLITE>
+
var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId + 1];
// Return canonical, localized namespace
+
if (typeof localizedNamespace !== 'undefined') {
Title.prototype.namespace=function() {
+
if (localizedNamespace === '') {
return mw.config.get('wgFormattedNamespaces')[this.namespaceId()];
+
this.value = this.stripNamespace();
};
+
} else {
Title.prototype.namespaceId=function() {
+
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
var n=this.value.indexOf(':');
+
}
if (n<0) { return 0; } //mainspace
+
return this.value;
var namespaceId = mw.config.get('wgNamespaceIds')[this.value.substring(0,n).split(' ').join('_').toLowerCase()];
 
if (typeof namespaceId=='undefined') return 0; //mainspace
 
return namespaceId;
 
};
 
//<NOLITE>
 
Title.prototype.talkPage=function() {
 
var t=new Title(this.value);
 
t.toTalkPage();
 
if (t.value) { return t; }
 
return null;
 
};
 
Title.prototype.isTalkPage=function() {
 
if (this.talkPage()===null) { return true; }
 
return false;
 
};
 
Title.prototype.toArticleFromTalkPage=function() {
 
//largely copy/paste from toTalkPage above.
 
if (this.value === null) { return null; }
 
 
var namespaceId = this.namespaceId();
 
if (namespaceId >= 0 && namespaceId % 2 == 1) //non-special and talk namespace
 
{
 
var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId-1];
 
if (typeof localizedNamespace!=='undefined')
 
{
 
if (localizedNamespace === '') {
 
this.value = this.stripNamespace();
 
} else {
 
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
 
 
}
 
}
return this.value;
 
 
}
 
}
}
 
  
this.value=null;
+
this.value = null;
return null;
+
return null;
};
+
};
Title.prototype.articleFromTalkPage=function() {
+
//</NOLITE>
var t=new Title(this.value);
+
// Return canonical, localized namespace
t.toArticleFromTalkPage();
+
Title.prototype.namespace = function () {
if (t.value) { return t; }
+
return mw.config.get('wgFormattedNamespaces')[this.namespaceId()];
return null;
+
};
};
+
Title.prototype.namespaceId = function () {
Title.prototype.articleFromTalkOrArticle=function() {
+
var n = this.value.indexOf(':');
var t=new Title(this.value);
+
if (n < 0) {
if ( t.toArticleFromTalkPage() ) { return t; }
+
return 0;
return this;
+
} //mainspace
};
+
var namespaceId =
Title.prototype.isIpUser=function() {
+
mw.config.get('wgNamespaceIds')[
return pg.re.ipUser.test(this.userName());
+
this.value.substring(0, n).split(' ').join('_').toLowerCase()
};
+
];
//</NOLITE>
+
if (typeof namespaceId == 'undefined') return 0; //mainspace
Title.prototype.stripNamespace=function(){ // returns a string, not a Title
+
return namespaceId;
var n=this.value.indexOf(':');
+
};
if (n<0) { return this.value; }
+
//<NOLITE>
var namespaceId = this.namespaceId();
+
Title.prototype.talkPage = function () {
if (namespaceId === pg.nsMainspaceId) return this.value;
+
var t = new Title(this.value);
return this.value.substring(n+1);
+
t.toTalkPage();
};
+
if (t.value) {
Title.prototype.setUtf=function(value){
+
return t;
if (!value) { this.value=''; return; }
+
}
var anch=value.indexOf('#');
+
return null;
if(anch < 0) { this.value=value.split('_').join(' '); this.anchor=''; return; }
+
};
this.value=value.substring(0,anch).split('_').join(' ');
+
Title.prototype.isTalkPage = function () {
this.anchor=value.substring(anch+1);
+
if (this.talkPage() === null) {
this.ns=null; // wait until namespace() is called
+
return true;
};
+
}
Title.prototype.setUrl=function(urlfrag) {
+
return false;
var anch=urlfrag.indexOf('#');
+
};
this.value=safeDecodeURI(urlfrag.substring(0,anch));
+
Title.prototype.toArticleFromTalkPage = function () {
this.anchor=value.substring(anch+1);
+
//largely copy/paste from toTalkPage above.
};
+
if (this.value === null) {
Title.prototype.append=function(x){
+
return null;
this.setUtf(this.value + x);
+
}
};
 
Title.prototype.urlString=function(x) {
 
if(!x) { x={}; }
 
var v=this.toString(true);
 
if (!x.omitAnchor && this.anchor) { v+= '#' + this.urlAnchor(); }
 
if (!x.keepSpaces) { v=v.split(' ').join('_'); }
 
return encodeURI(v).split('&').join('%26').split('?').join('%3F').split('+').join('%2B');
 
};
 
Title.prototype.removeAnchor=function() {
 
return new Title(this.toString(true));
 
};
 
Title.prototype.toUrl=function() {
 
return pg.wiki.titlebase + this.urlString();
 
};
 
  
 +
var namespaceId = this.namespaceId();
 +
if (namespaceId >= 0 && namespaceId % 2 == 1) {
 +
//non-special and talk namespace
 +
var localizedNamespace = mw.config.get('wgFormattedNamespaces')[namespaceId - 1];
 +
if (typeof localizedNamespace !== 'undefined') {
 +
if (localizedNamespace === '') {
 +
this.value = this.stripNamespace();
 +
} else {
 +
this.value = localizedNamespace.split(' ').join('_') + ':' + this.stripNamespace();
 +
}
 +
return this.value;
 +
}
 +
}
  
function paramValue(param, url) {
+
this.value = null;
var s=url.parenSplit(RegExp('[?&]' + literalizeRegex(param) + '=([^?&]*)'));
+
return null;
if (!url) { return null; }
+
};
return s[1] || null;
+
Title.prototype.articleFromTalkPage = function () {
}
+
var t = new Title(this.value);
 
+
t.toArticleFromTalkPage();
function parseParams(url) {
+
if (t.value) {
var specialDiff = pg.re.specialdiff.exec(url);
+
return t;
if (specialDiff)
+
}
{
+
return null;
var split= specialDiff[1].split('/');
+
};
if (split.length==1) return {oldid:split[0], diff: 'prev'};
+
Title.prototype.articleFromTalkOrArticle = function () {
else if (split.length==2) return {oldid: split[0], diff: split[1]};
+
var t = new Title(this.value);
}
+
if (t.toArticleFromTalkPage()) {
 
+
return t;
var ret={};
+
}
if (url.indexOf('?')==-1) { return ret; }
+
return this;
url = url.split('#')[0];
+
};
var s=url.split('?').slice(1).join();
+
Title.prototype.isIpUser = function () {
var t=s.split('&');
+
return pg.re.ipUser.test(this.userName());
for (var i=0; i<t.length; ++i) {
+
};
var z=t[i].split('=');
+
//</NOLITE>
z.push(null);
+
Title.prototype.stripNamespace = function () {
ret[z[0]]=z[1];
+
// returns a string, not a Title
}
+
var n = this.value.indexOf(':');
//Diff revision with no oldid is interpreted as a diff to the previous revision by MediaWiki
+
if (n < 0) {
if (ret.diff && typeof(ret.oldid)==='undefined')
+
return this.value;
{
+
}
ret.oldid = "prev";
+
var namespaceId = this.namespaceId();
}
+
if (namespaceId === pg.nsMainspaceId) return this.value;
//Documentation seems to say something different, but oldid can also accept prev/next, and Echo is emitting such URLs. Simple fixup during parameter decoding:
+
return this.value.substring(n + 1);
if (ret.oldid && (ret.oldid==='prev' || ret.oldid==='next' || ret.oldid==='cur'))
+
};
{
+
Title.prototype.setUtf = function (value) {
var helper = ret.diff;
+
if (!value) {
ret.diff = ret.oldid;
+
this.value = '';
ret.oldid = helper;
+
return;
}
+
}
return ret;
+
var anch = value.indexOf('#');
}
+
if (anch < 0) {
 +
this.value = value.split('_').join(' ');
 +
this.anchor = '';
 +
return;
 +
}
 +
this.value = value.substring(0, anch).split('_').join(' ');
 +
this.anchor = value.substring(anch + 1);
 +
this.ns = null; // wait until namespace() is called
 +
};
 +
Title.prototype.setUrl = function (urlfrag) {
 +
var anch = urlfrag.indexOf('#');
 +
this.value = safeDecodeURI(urlfrag.substring(0, anch));
 +
this.anchor = this.value.substring(anch + 1);
 +
};
 +
Title.prototype.append = function (x) {
 +
this.setUtf(this.value + x);
 +
};
 +
Title.prototype.urlString = function (x) {
 +
if (!x) {
 +
x = {};
 +
}
 +
var v = this.toString(true);
 +
if (!x.omitAnchor && this.anchor) {
 +
v += '#' + this.urlAnchor();
 +
}
 +
if (!x.keepSpaces) {
 +
v = v.split(' ').join('_');
 +
}
 +
return encodeURI(v).split('&').join('%26').split('?').join('%3F').split('+').join('%2B');
 +
};
 +
Title.prototype.removeAnchor = function () {
 +
return new Title(this.toString(true));
 +
};
 +
Title.prototype.toUrl = function () {
 +
return pg.wiki.titlebase + this.urlString();
 +
};
  
// all sorts of stuff here
+
function parseParams(url) {
// FIXME almost everything needs to be rewritten
+
var specialDiff = pg.re.specialdiff.exec(url);
 +
if (specialDiff) {
 +
var split = specialDiff[1].split('/');
 +
if (split.length == 1) return { oldid: split[0], diff: 'prev' };
 +
else if (split.length == 2) return { oldid: split[0], diff: split[1] };
 +
}
  
function oldidFromAnchor(a) { return paramValue('oldid', a.href); }
+
var ret = {};
//function diffFromAnchor(a) { return paramValue('diff', a.href); }
+
if (url.indexOf('?') == -1) {
 
+
return ret;
 
+
}
function wikiMarkupToAddressFragment (str) { // for images
+
url = url.split('#')[0];
var ret = safeDecodeURI(str);
+
var s = url.split('?').slice(1).join();
ret = ret.split(' ').join('_');
+
var t = s.split('&');
ret = encodeURI(ret);
+
for (var i = 0; i < t.length; ++i) {
return ret;
+
var z = t[i].split('=');
}
+
z.push(null);
 
+
ret[z[0]] = z[1];
// (a) myDecodeURI (first standard decodeURI, then pg.re.urlNoPopup)
+
}
// (b) change spaces to underscores
+
//Diff revision with no oldid is interpreted as a diff to the previous revision by MediaWiki
// (c) encodeURI (just the straight one, no pg.re.urlNoPopup)
+
if (ret.diff && typeof ret.oldid === 'undefined') {
 
+
ret.oldid = 'prev';
function myDecodeURI (str) {
+
}
var ret;
+
//Documentation seems to say something different, but oldid can also accept prev/next, and
// FIXME decodeURIComponent??
+
//Echo is emitting such URLs. Simple fixup during parameter decoding:
try { ret=decodeURI(str.toString()); }
+
if (ret.oldid && (ret.oldid === 'prev' || ret.oldid === 'next' || ret.oldid === 'cur')) {
catch (summat) { return str; }
+
var helper = ret.diff;
for (var i=0; i<pg.misc.decodeExtras.length; ++i) {
+
ret.diff = ret.oldid;
var from=pg.misc.decodeExtras[i].from;
+
ret.oldid = helper;
var to=pg.misc.decodeExtras[i].to;
+
}
ret=ret.split(from).join(to);
+
return ret;
 
}
 
}
return ret;
 
}
 
  
function safeDecodeURI(str) { var ret=myDecodeURI(str); return ret || str; }
+
// (a) myDecodeURI (first standard decodeURI, then pg.re.urlNoPopup)
 +
// (b) change spaces to underscores
 +
// (c) encodeURI (just the straight one, no pg.re.urlNoPopup)
  
///////////
+
function myDecodeURI(str) {
// TESTS //
+
var ret;
///////////
+
// FIXME decodeURIComponent??
 
+
try {
//<NOLITE>
+
ret = decodeURI(str.toString());
function isIpUser(user) {return pg.re.ipUser.test(user);}
+
} catch (summat) {
 
+
return str;
function isDisambig(data, article) {
+
}
if (!getValueOf('popupAllDabsStubs') && article.namespace()) { return false; }
+
for (var i = 0; i < pg.misc.decodeExtras.length; ++i) {
return ! article.isTalkPage() && pg.re.disambig.test(data);
+
var from = pg.misc.decodeExtras[i].from;
}
+
var to = pg.misc.decodeExtras[i].to;
 
+
ret = ret.split(from).join(to);
function stubCount(data, article) {
 
if (!getValueOf('popupAllDabsStubs') && article.namespace()) { return false; }
 
var sectStub=0;
 
var realStub=0;
 
if (pg.re.stub.test(data)) {
 
var s=data.parenSplit(pg.re.stub);
 
for (var i=1; i<s.length; i=i+2) {
 
if (s[i]) { ++sectStub; }
 
else { ++realStub; }
 
 
}
 
}
 +
return ret;
 
}
 
}
return { real: realStub, sect: sectStub };
 
}
 
  
function isValidImageName(str){ // extend as needed...
+
function safeDecodeURI(str) {
return ( str.indexOf('{') == -1 );
+
var ret = myDecodeURI(str);
}
+
return ret || str;
 +
}
  
function isInStrippableNamespace(article) {
+
///////////
//I believe that this method means to return whether the given article is in a namspace without subpages. Meaning, it's broken.
+
// TESTS //
return ( article.namespace() !== '' );
+
///////////
}
 
  
function isInMainNamespace(article) { return !isInStrippableNamespace(article); }
+
//<NOLITE>
 
+
function isDisambig(data, article) {
function anchorContainsImage(a) {
+
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
// iterate over children of anchor a
+
return false;
// see if any are images
 
if (a === null) { return false; }
 
kids=a.childNodes;
 
for (var i=0; i<kids.length; ++i) { if (kids[i].nodeName=='IMG') { return true; } }
 
return false;
 
}
 
//</NOLITE>
 
function isPopupLink(a) {
 
// NB for performance reasons, TOC links generally return true
 
// they should be stripped out later
 
 
 
if (!markNopopupSpanLinks.done) { markNopopupSpanLinks(); }
 
if (a.inNopopupSpan) { return false; }
 
 
 
// FIXME is this faster inline?
 
if (a.onmousedown || a.getAttribute('nopopup')) { return false; }
 
var h=a.href;
 
if (h === document.location.href+'#') { return false; }
 
if (!pg.re.basenames.test(h)) { return false; }
 
if (!pg.re.urlNoPopup.test(h)) { return true; }
 
return (
 
(pg.re.email.test(h) || pg.re.contribs.test(h) || pg.re.backlinks.test(h) || pg.re.specialdiff.test(h)) &&
 
h.indexOf('&limit=') == -1 );
 
}
 
 
 
function markNopopupSpanLinks() {
 
if( !getValueOf('popupOnlyArticleLinks'))
 
fixVectorMenuPopups();
 
 
 
var s = $('.nopopups').toArray();
 
for (var i=0; i<s.length; ++i) {
 
var as=s[i].getElementsByTagName('a');
 
for (var j=0; j<as.length; ++j) {
 
as[j].inNopopupSpan=true;
 
 
}
 
}
 +
return !article.isTalkPage() && pg.re.disambig.test(data);
 
}
 
}
 
markNopopupSpanLinks.done=true;
 
}
 
  
function fixVectorMenuPopups() {
+
function stubCount(data, article) {
$('div.vectorMenu h3:first a:first').prop('inNopopupSpan', true);
+
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
}
+
return false;
// ENDFILE: titles.js
 
// STARTFILE: cookies.js
 
//<NOLITE>
 
//////////////////////////////////////////////////
 
// Cookie handling
 
// from http://www.quirksmode.org/js/cookies.html
 
 
 
var Cookie= {
 
create: function(name,value,days)
 
{
 
var expires;
 
if (days)
 
{
 
var date = new Date();
 
date.setTime(date.getTime()+(days*24*60*60*1000));
 
expires = "; expires="+date.toGMTString();
 
 
}
 
}
else { expires = ""; }
+
var sectStub = 0;
document.cookie = name+"="+value+expires+"; path=/";
+
var realStub = 0;
},
+
if (pg.re.stub.test(data)) {
 
+
var s = data.parenSplit(pg.re.stub);
read: function(name)
+
for (var i = 1; i < s.length; i = i + 2) {
{
+
if (s[i]) {
var nameEQ = name + "=";
+
++sectStub;
var ca = document.cookie.split(';');
+
} else {
for(var i=0;i < ca.length;i++)
+
++realStub;
{
+
}
var c = ca[i];
+
}
while (c.charAt(0)==' ') { c = c.substring(1,c.length); }
 
if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); }
 
 
}
 
}
return null;
+
return { real: realStub, sect: sectStub };
},
+
}
  
erase: function(name)
+
function isValidImageName(str) {
{
+
// extend as needed...
Cookie.create(name,"",-1);
+
return str.indexOf('{') == -1;
 
}
 
}
};
 
//</NOLITE>
 
// ENDFILE: cookies.js
 
// STARTFILE: getpage.js
 
//////////////////////////////////////////////////
 
// Wiki-specific downloading
 
//
 
 
// Schematic for a getWiki call
 
//
 
//  getWiki->-getPageWithCaching
 
// |
 
//   false |   true
 
// getPage<-[findPictureInCache]->-onComplete(a fake download)
 
//  \.
 
// (async)->addPageToCache(download)->-onComplete(download)
 
  
 
+
function isInStrippableNamespace(article) {
/** @todo {document}
+
// Does the namespace allow subpages
@param {Title} article
+
// Note, would be better if we had access to wgNamespacesWithSubpages
@param {Function} onComplete
+
return article.namespaceId() !== 0;
@param {integer} oldid
 
@param {Navapopup} owner
 
*/
 
function getWiki(article, onComplete, oldid, owner) {
 
// set ctype=text/css to get around opera gzip bug
 
var url = pg.wiki.titlebase;
 
if (article.namespaceId() >= 0)
 
url += article.removeAnchor().urlString();
 
if (oldid || oldid === 0 || oldid==='0')
 
url += '&oldid='+oldid;
 
url += '&action=raw';
 
 
 
getPageWithCaching(url, onComplete, owner);
 
}
 
 
 
// check cache to see if page exists
 
 
 
function getPageWithCaching(url, onComplete, owner) {
 
log('getPageWithCaching, url='+url);
 
var i=findInPageCache(url);
 
var d;
 
if (i > -1) {
 
d=fakeDownload(url, owner.idNumber, onComplete,
 
pg.cache.pages[i].data, pg.cache.pages[i].lastModified,
 
owner);
 
} else {
 
d=getPage(url, onComplete, owner);
 
if (d && owner && owner.addDownload) {
 
owner.addDownload(d);
 
d.owner=owner;
 
}
 
 
}
 
}
}
 
  
function getPage(url, onComplete, owner) {
+
function isInMainNamespace(article) {
log('getPage');
+
return article.namespaceId() === 0;
var callback= function (d) { if (!d.aborted) {addPageToCache(d); onComplete(d);} };
 
return startDownload(url, owner.idNumber, callback);
 
}
 
 
 
function findInPageCache(url) {
 
for (var i=0; i<pg.cache.pages.length; ++i) {
 
if (url==pg.cache.pages[i].url) { return i; }
 
 
}
 
}
return -1;
 
}
 
  
function addPageToCache(download) {
+
function anchorContainsImage(a) {
log('addPageToCache '+download.url);
+
// iterate over children of anchor a
var page = {url: download.url, data: download.data, lastModified: download.lastModified};
+
// see if any are images
return pg.cache.pages.push(page);
+
if (a === null) {
}
+
return false;
// ENDFILE: getpage.js
+
}
// STARTFILE: md5-2.2alpha.js
+
var kids = a.childNodes;
//<NOLITE>
+
for (var i = 0; i < kids.length; ++i) {
/*
+
if (kids[i].nodeName == 'IMG') {
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+
return true;
* Digest Algorithm, as defined in RFC 1321.
+
}
* Version 2.2-alpha Copyright (C) Paul Johnston 1999 - 2005
+
}
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+
return false;
* Distributed under the BSD License
+
}
* See http://pajhome.org.uk/crypt/md5 for more info.
+
//</NOLITE>
*/
+
function isPopupLink(a) {
 +
// NB for performance reasons, TOC links generally return true
 +
// they should be stripped out later
  
/*
+
if (!markNopopupSpanLinks.done) {
* Configurable variables. You may need to tweak these to be compatible with
+
markNopopupSpanLinks();
* the server-side, but the defaults work in most cases.
+
}
*/
+
if (a.inNopopupSpan) {
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase */
+
return false;
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance  */
+
}
  
/*
+
// FIXME is this faster inline?
* These are the functions you'll usually want to call
+
if (a.onmousedown || a.getAttribute('nopopup')) {
* They take string arguments and return either hex or base-64 encoded strings
+
return false;
*/
+
}
function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
+
var h = a.href;
function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
+
if (h === document.location.href + '#') {
function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
+
return false;
function hex_hmac_md5(k, d)
+
}
  { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+
if (!pg.re.basenames.test(h)) {
function b64_hmac_md5(k, d)
+
return false;
  { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+
}
function any_hmac_md5(k, d, e)
+
if (!pg.re.urlNoPopup.test(h)) {
  { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
+
return true;
 +
}
 +
return (
 +
(pg.re.email.test(h) ||
 +
pg.re.contribs.test(h) ||
 +
pg.re.backlinks.test(h) ||
 +
pg.re.specialdiff.test(h)) &&
 +
h.indexOf('&limit=') == -1
 +
);
 +
}
  
/*
+
function markNopopupSpanLinks() {
* Perform a simple self-test to see if the VM is working
+
if (!getValueOf('popupOnlyArticleLinks')) fixVectorMenuPopups();
*/
 
function md5_vm_test()
 
{
 
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
 
}
 
  
/*
+
var s = $('.nopopups').toArray();
* Calculate the MD5 of a raw string
+
for (var i = 0; i < s.length; ++i) {
*/
+
var as = s[i].getElementsByTagName('a');
function rstr_md5(s)
+
for (var j = 0; j < as.length; ++j) {
{
+
as[j].inNopopupSpan = true;
  return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+
}
}
+
}
 
 
/*
 
* Calculate the HMAC-MD5, of a key and some data (raw strings)
 
*/
 
function rstr_hmac_md5(key, data)
 
{
 
  var bkey = rstr2binl(key);
 
  if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
 
 
 
  var ipad = Array(16), opad = Array(16);
 
  for(var i = 0; i < 16; i++)
 
  {
 
ipad[i] = bkey[i] ^ 0x36363636;
 
opad[i] = bkey[i] ^ 0x5C5C5C5C;
 
  }
 
 
 
  var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
 
  return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
 
}
 
 
 
/*
 
* Convert a raw string to a hex string
 
*/
 
function rstr2hex(input)
 
{
 
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
 
  var output = "";
 
  var x;
 
  for(var i = 0; i < input.length; i++)
 
  {
 
x = input.charCodeAt(i);
 
output += hex_tab.charAt((x >>> 4) & 0x0F) +
 
          hex_tab.charAt( x       & 0x0F);
 
  }
 
  return output;
 
}
 
  
/*
+
markNopopupSpanLinks.done = true;
* Convert a raw string to a base-64 string
 
*/
 
function rstr2b64(input)
 
{
 
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 
  var output = "";
 
  var len = input.length;
 
  for(var i = 0; i < len; i += 3)
 
  {
 
var triplet = (input.charCodeAt(i) << 16)
 
| (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
 
| (i + 2 < len ? input.charCodeAt(i+2) : 0);
 
for(var j = 0; j < 4; j++)
 
{
 
  if(i * 8 + j * 6 > input.length * 8) output += b64pad;
 
  else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
 
 
}
 
}
  }
 
  return output;
 
}
 
  
/*
+
function fixVectorMenuPopups() {
* Convert a raw string to an arbitrary string encoding
+
$('nav.vector-menu h3:first a:first').prop('inNopopupSpan', true);
*/
 
function rstr2any(input, encoding)
 
{
 
  var divisor = encoding.length;
 
  var remainders = Array();
 
  var i, q, x, quotient;
 
 
 
  /* Convert to an array of 16-bit big-endian values, forming the dividend */
 
  var dividend = Array(input.length / 2);
 
  for(i = 0; i < dividend.length; i++)
 
  {
 
dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
 
  }
 
 
 
  /*
 
  * Repeatedly perform a long division. The binary array forms the dividend,
 
  * the length of the encoding is the divisor. Once computed, the quotient
 
  * forms the dividend for the next step. We stop when the dividend is zero.
 
  * All remainders are stored for later use.
 
  */
 
  while(dividend.length > 0)
 
  {
 
quotient = Array();
 
x = 0;
 
for(i = 0; i < dividend.length; i++)
 
{
 
  x = (x << 16) + dividend[i];
 
  q = Math.floor(x / divisor);
 
  x -= q * divisor;
 
  if(quotient.length > 0 || q > 0)
 
quotient[quotient.length] = q;
 
 
}
 
}
remainders[remainders.length] = x;
+
// ENDFILE: titles.js
dividend = quotient;
 
  }
 
  
  /* Convert the remainders to the output string */
+
// STARTFILE: getpage.js
  var output = "";
+
//////////////////////////////////////////////////
  for(i = remainders.length - 1; i >= 0; i--)
+
// Wiki-specific downloading
output += encoding.charAt(remainders[i]);
+
//
  
   return output;
+
// Schematic for a getWiki call
}
+
//
 +
//            getPageWithCaching
 +
// |
 +
//   false |   true
 +
// getPage<-[findPictureInCache]->-onComplete(a fake download)
 +
//   \.
 +
// (async)->addPageToCache(download)->-onComplete(download)
  
/*
+
// check cache to see if page exists
* Encode a string as utf-8.
 
* For efficiency, this assumes the input is valid utf-16.
 
*/
 
function str2rstr_utf8(input)
 
{
 
  var output = "";
 
  var i = -1;
 
  var x, y;
 
  
  while(++i < input.length)
+
function getPageWithCaching(url, onComplete, owner) {
  {
+
log('getPageWithCaching, url=' + url);
/* Decode utf-16 surrogate pairs */
+
var i = findInPageCache(url);
x = input.charCodeAt(i);
+
var d;
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
+
if (i > -1) {
if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
+
d = fakeDownload(
{
+
url,
  x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
+
owner.idNumber,
  i++;
+
onComplete,
 +
pg.cache.pages[i].data,
 +
pg.cache.pages[i].lastModified,
 +
owner
 +
);
 +
} else {
 +
d = getPage(url, onComplete, owner);
 +
if (d && owner && owner.addDownload) {
 +
owner.addDownload(d);
 +
d.owner = owner;
 +
}
 +
}
 
}
 
}
  
/* Encode output as utf-8 */
+
function getPage(url, onComplete, owner) {
if(x <= 0x7F)
+
log('getPage');
  output += String.fromCharCode(x);
+
var callback = function (d) {
else if(x <= 0x7FF)
+
if (!d.aborted) {
  output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
+
addPageToCache(d);
0x80 | ( x   & 0x3F));
+
onComplete(d);
else if(x <= 0xFFFF)
+
}
  output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
+
};
0x80 | ((x >>> 6 ) & 0x3F),
+
return startDownload(url, owner.idNumber, callback);
0x80 | ( x   & 0x3F));
+
}
else if(x <= 0x1FFFFF)
 
  output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
 
0x80 | ((x >>> 12) & 0x3F),
 
0x80 | ((x >>> 6 ) & 0x3F),
 
0x80 | ( x   & 0x3F));
 
  }
 
  return output;
 
}
 
 
 
/*
 
* Encode a string as utf-16
 
*/
 
function str2rstr_utf16le(input)
 
{
 
  var output = "";
 
  for(var i = 0; i < input.length; i++)
 
output += String.fromCharCode( input.charCodeAt(i)   & 0xFF,
 
  (input.charCodeAt(i) >>> 8) & 0xFF);
 
  return output;
 
}
 
 
 
function str2rstr_utf16be(input)
 
{
 
  var output = "";
 
  for(var i = 0; i < input.length; i++)
 
output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
 
  input.charCodeAt(i)   & 0xFF);
 
  return output;
 
}
 
 
 
/*
 
* Convert a raw string to an array of little-endian words
 
* Characters >255 have their high-byte silently ignored.
 
*/
 
function rstr2binl(input)
 
{
 
  var output = Array(input.length >> 2);
 
  for(var i = 0; i < output.length; i++)
 
output[i] = 0;
 
  for(i = 0; i < input.length * 8; i += 8)
 
output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
 
  return output;
 
}
 
 
 
/*
 
* Convert an array of little-endian words to a string
 
*/
 
function binl2rstr(input)
 
{
 
  var output = "";
 
  for(var i = 0; i < input.length * 32; i += 8)
 
output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
 
  return output;
 
}
 
 
 
/*
 
* Calculate the MD5 of an array of little-endian words, and a bit length.
 
*/
 
function binl_md5(x, len)
 
{
 
  /* append padding */
 
  x[len >> 5] |= 0x80 << ((len) % 32);
 
  x[(((len + 64) >>> 9) << 4) + 14] = len;
 
 
 
  var a =  1732584193;
 
  var b = -271733879;
 
  var c = -1732584194;
 
  var d =  271733878;
 
 
 
  for(var i = 0; i < x.length; i += 16)
 
  {
 
var olda = a;
 
var oldb = b;
 
var oldc = c;
 
var oldd = d;
 
 
 
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
 
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
 
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
 
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
 
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
 
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
 
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
 
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
 
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
 
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
 
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
 
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
 
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
 
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
 
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
 
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
 
 
 
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
 
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
 
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
 
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
 
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
 
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
 
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
 
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
 
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
 
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
 
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
 
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
 
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
 
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
 
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
 
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
 
 
 
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
 
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
 
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
 
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
 
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
 
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
 
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
 
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
 
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
 
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
 
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
 
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
 
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
 
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
 
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
 
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
 
 
 
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
 
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
 
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
 
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
 
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
 
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
 
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
 
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
 
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
 
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
 
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
 
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
 
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
 
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
 
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
 
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
 
 
 
a = safe_add(a, olda);
 
b = safe_add(b, oldb);
 
c = safe_add(c, oldc);
 
d = safe_add(d, oldd);
 
  }
 
  return Array(a, b, c, d);
 
}
 
 
 
/*
 
* These functions implement the four basic operations the algorithm uses.
 
*/
 
function md5_cmn(q, a, b, x, s, t)
 
{
 
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
 
}
 
function md5_ff(a, b, c, d, x, s, t)
 
{
 
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
 
}
 
function md5_gg(a, b, c, d, x, s, t)
 
{
 
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
 
}
 
function md5_hh(a, b, c, d, x, s, t)
 
{
 
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
 
}
 
function md5_ii(a, b, c, d, x, s, t)
 
{
 
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
 
}
 
 
 
/*
 
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
 
* to work around bugs in some JS interpreters.
 
*/
 
function safe_add(x, y)
 
{
 
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
 
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
 
  return (msw << 16) | (lsw & 0xFFFF);
 
}
 
 
 
/*
 
* Bitwise rotate a 32-bit number to the left.
 
*/
 
function bit_rol(num, cnt)
 
{
 
  return (num << cnt) | (num >>> (32 - cnt));
 
}
 
//</NOLITE>
 
// ENDFILE: md5-2.2alpha.js
 
// STARTFILE: parensplit.js
 
//////////////////////////////////////////////////
 
// parenSplit
 
 
 
// String.prototype.parenSplit should do what ECMAscript says String.prototype.split does,
 
// interspersing paren matches (regex capturing groups) between the split elements.
 
// i.e. 'abc'.split(/(b)/)) should return ['a','b','c'], not ['a','c']
 
  
if (String('abc'.split(/(b)/))!='a,b,c') {
+
function findInPageCache(url) {
// broken String.split, e.g. konq, IE
+
for (var i = 0; i < pg.cache.pages.length; ++i) {
String.prototype.parenSplit=function (re) {
+
if (url == pg.cache.pages[i].url) {
re=nonGlobalRegex(re);
+
return i;
var s=this;
 
var m=re.exec(s);
 
var ret=[];
 
while (m && s) {
 
// without the following loop, we have
 
// 'ab'.parenSplit(/a|(b)/) != 'ab'.split(/a|(b)/)
 
for(var i=0; i<m.length; ++i) {
 
if (typeof m[i]=='undefined') m[i]='';
 
 
}
 
}
ret.push(s.substring(0,m.index));
 
ret = ret.concat(m.slice(1));
 
s=s.substring(m.index + m[0].length);
 
m=re.exec(s);
 
 
}
 
}
ret.push(s);
+
return -1;
return ret;
+
}
};
 
} else {
 
String.prototype.parenSplit=function (re) { return this.split(re); };
 
String.prototype.parenSplit.isNative=true;
 
}
 
  
function nonGlobalRegex(re) {
+
function addPageToCache(download) {
var s=re.toString();
+
log('addPageToCache ' + download.url);
flags='';
+
var page = {
for (var j=s.length; s.charAt(j) != '/'; --j) {
+
url: download.url,
if (s.charAt(j) != 'g') { flags += s.charAt(j); }
+
data: download.data,
 +
lastModified: download.lastModified,
 +
};
 +
return pg.cache.pages.push(page);
 
}
 
}
var t=s.substring(1,j);
+
// ENDFILE: getpage.js
return RegExp(t,flags);
 
}
 
// ENDFILE: parensplit.js
 
// STARTFILE: tools.js
 
// IE madness with encoding
 
// ========================
 
//
 
// suppose throughout that the page is in utf8, like wikipedia
 
//
 
// if a is an anchor DOM element and a.href should consist of
 
//
 
// http://host.name.here/wiki/foo?bar=baz
 
//
 
// then IE gives foo as "latin1-encoded" utf8; we have foo = decode_utf8(decodeURI(foo_ie))
 
// but IE gives bar=baz correctly as plain utf8
 
//
 
// ---------------------------------
 
//
 
// IE's xmlhttp doesn't understand utf8 urls. Have to use encodeURI here.
 
//
 
// ---------------------------------
 
//
 
// summat else
 
  
// Source: http://aktuell.de.selfhtml.org/artikel/javascript/utf8b64/utf8.htm
+
// STARTFILE: parensplit.js
 +
//////////////////////////////////////////////////
 +
// parenSplit
  
//<NOLITE>
+
// String.prototype.parenSplit should do what ECMAscript says String.prototype.split does,
function encode_utf8(rohtext) {
+
// interspersing paren matches (regex capturing groups) between the split elements.
// dient der Normalisierung des Zeilenumbruchs
+
// i.e. 'abc'.split(/(b)/)) should return ['a','b','c'], not ['a','c']
rohtext = rohtext.replace(/\r\n/g,"\n");
+
 
var utftext = "";
+
if (String('abc'.split(/(b)/)) != 'a,b,c') {
for(var n=0; n<rohtext.length; n++)
+
// broken String.split, e.g. konq, IE < 10
{
+
String.prototype.parenSplit = function (re) {
// ermitteln des Unicodes des  aktuellen Zeichens
+
re = nonGlobalRegex(re);
var c=rohtext.charCodeAt(n);
+
var s = this;
// alle Zeichen von 0-127 => 1byte
+
var m = re.exec(s);
if (c<128)
+
var ret = [];
utftext += String.fromCharCode(c);
+
while (m && s) {
// alle Zeichen von 127 bis 2047 => 2byte
+
// without the following loop, we have
else if((c>127) && (c<2048)) {
+
// 'ab'.parenSplit(/a|(b)/) != 'ab'.split(/a|(b)/)
utftext += String.fromCharCode((c>>6)|192);
+
for (var i = 0; i < m.length; ++i) {
utftext += String.fromCharCode((c&63)|128);}
+
if (typeof m[i] == 'undefined') m[i] = '';
// alle Zeichen von 2048 bis 66536 => 3byte
+
}
else {
+
ret.push(s.substring(0, m.index));
utftext += String.fromCharCode((c>>12)|224);
+
ret = ret.concat(m.slice(1));
utftext += String.fromCharCode(((c>>6)&63)|128);
+
s = s.substring(m.index + m[0].length);
utftext += String.fromCharCode((c&63)|128);}
+
m = re.exec(s);
 +
}
 +
ret.push(s);
 +
return ret;
 +
};
 +
} else {
 +
String.prototype.parenSplit = function (re) {
 +
return this.split(re);
 +
};
 +
String.prototype.parenSplit.isNative = true;
 
}
 
}
return utftext;
 
}
 
  
function getJsObj(json) {
+
function nonGlobalRegex(re) {
try {
+
var s = re.toString();
var json_ret = JSON.parse(json);
+
var flags = '';
if( json_ret.warnings ) {
+
for (var j = s.length; s.charAt(j) != '/'; --j) {
for( var w=0; w < json_ret.warnings.length; w++ ) {
+
if (s.charAt(j) != 'g') {
log( json_ret.warnings[w]['*'] );
+
flags += s.charAt(j);
 
}
 
}
} else if ( json_ret.error ) {
 
errlog( json_ret.error.code + ': ' + json_ret.error.info );
 
 
}
 
}
return json_ret;
+
var t = s.substring(1, j);
} catch (someError) {
+
return RegExp(t, flags);
errlog('Something went wrong with getJsobj, json='+json);
 
return 1;
 
 
}
 
}
}
+
// ENDFILE: parensplit.js
  
function anyChild(obj) {
+
// STARTFILE: tools.js
for (var p in obj) {
+
// IE madness with encoding
return obj[p];
+
// ========================
}
+
//
return null;
+
// suppose throughout that the page is in utf8, like wikipedia
}
+
//
 +
// if a is an anchor DOM element and a.href should consist of
 +
//
 +
// http://host.name.here/wiki/foo?bar=baz
 +
//
 +
// then IE gives foo as "latin1-encoded" utf8; we have foo = decode_utf8(decodeURI(foo_ie))
 +
// but IE gives bar=baz correctly as plain utf8
 +
//
 +
// ---------------------------------
 +
//
 +
// IE's xmlhttp doesn't understand utf8 urls. Have to use encodeURI here.
 +
//
 +
// ---------------------------------
 +
//
 +
// summat else
  
//</NOLITE>
+
// Source: http://aktuell.de.selfhtml.org/artikel/javascript/utf8b64/utf8.htm
  
function decode_utf8(utftext) {
+
//<NOLITE>
var plaintext = ""; var i=0, c=0, c1=0, c2=0;
+
 
// while-Schleife, weil einige Zeichen uebersprungen werden
+
function getJsObj(json) {
while(i<utftext.length)
+
try {
{
+
var json_ret = JSON.parse(json);
c = utftext.charCodeAt(i);
+
if (json_ret.warnings) {
if (c<128) {
+
for (var w = 0; w < json_ret.warnings.length; w++) {
plaintext += String.fromCharCode(c);
+
if (json_ret.warnings[w]['*']) {
i++;}
+
log(json_ret.warnings[w]['*']);
else if((c>191) && (c<224)) {
+
} else {
c2 = utftext.charCodeAt(i+1);
+
log(json_ret.warnings[w].warnings);
plaintext += String.fromCharCode(((c&31)<<6) | (c2&63));
+
}
i+=2;}
+
}
else {
+
} else if (json_ret.error) {
c2 = utftext.charCodeAt(i+1); c3 = utftext.charCodeAt(i+2);
+
errlog(json_ret.error.code + ': ' + json_ret.error.info);
plaintext += String.fromCharCode(((c&15)<<12) | ((c2&63)<<6) | (c3&63));
+
}
i+=3;}
+
return json_ret;
 +
} catch (someError) {
 +
errlog('Something went wrong with getJsObj, json=' + json);
 +
return 1;
 +
}
 
}
 
}
return plaintext;
 
}
 
  
 +
function anyChild(obj) {
 +
for (var p in obj) {
 +
return obj[p];
 +
}
 +
return null;
 +
}
  
function upcaseFirst(str) {
+
//</NOLITE>
if (typeof str != typeof '' || str === '') return '';
 
return str.charAt(0).toUpperCase() + str.substring(1);
 
}
 
  
 +
function upcaseFirst(str) {
 +
if (typeof str != typeof '' || str === '') return '';
 +
return str.charAt(0).toUpperCase() + str.substring(1);
 +
}
  
function findInArray(arr, foo) {
+
function findInArray(arr, foo) {
if (!arr || !arr.length) { return -1; }
+
if (!arr || !arr.length) {
var len=arr.length;
+
return -1;
for (var i=0; i<len; ++i) { if (arr[i]==foo) { return i; } }
+
}
return -1;
+
var len = arr.length;
}
+
for (var i = 0; i < len; ++i) {
 +
if (arr[i] == foo) {
 +
return i;
 +
}
 +
}
 +
return -1;
 +
}
  
function nextOne (array, value) {
+
/* eslint-disable no-unused-vars */
// NB if the array has two consecutive entries equal
+
function nextOne(array, value) {
// then this will loop on successive calls
+
// NB if the array has two consecutive entries equal
var i=findInArray(array, value);
+
// then this will loop on successive calls
if (i<0) { return null; }
+
var i = findInArray(array, value);
return array[i+1];
+
if (i < 0) {
}
+
return null;
 +
}
 +
return array[i + 1];
 +
}
 +
/* eslint-enable no-unused-vars */
  
function literalizeRegex(str){
+
function literalizeRegex(str) {
return str.replace(RegExp('([-.|()\\\\+?*^${}\\[\\]])', 'g'), '\\$1');
+
return mw.util.escapeRegExp(str);
}
+
}
  
String.prototype.entify=function() {
+
String.prototype.entify = function () {
//var shy='&shy;';
+
//var shy='&shy;';
return this.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;'/*+shy*/).split('"').join('&quot;');
+
return this.split('&')
};
+
.join('&amp;')
 +
.split('<')
 +
.join('&lt;')
 +
.split('>')
 +
.join('&gt;' /*+shy*/)
 +
.split('"')
 +
.join('&quot;');
 +
};
  
function findThis(array, value) {
+
// Array filter function
if (typeof array.length == 'undefined') { return null; }
+
function removeNulls(val) {
for (var i=0; i<array.length; ++i) {
+
return val !== null;
if (array[i]==value) { return i; }
 
 
}
 
}
return null;
 
}
 
  
function removeNulls(list) {
+
function joinPath(list) {
var ret=[];
+
return list.filter(removeNulls).join('/');
for (var i=0; i<list.length; ++i) {
 
if (list[i]) {
 
ret.push(list[i]);
 
}
 
 
}
 
}
return ret;
 
}
 
function joinPath(list) {
 
return removeNulls(list).join('/');
 
}
 
  
 
+
function simplePrintf(str, subs) {
function simplePrintf(str, subs) {
+
if (!str || !subs) {
if (!str || !subs) { return str; }
+
return str;
var ret=[];
 
var s=str.parenSplit(/(%s|\$[0-9]+)/);
 
var i=0;
 
do {
 
ret.push(s.shift());
 
if ( !s.length ) { break; }
 
var cmd=s.shift();
 
if (cmd == '%s') {
 
if ( i < subs.length ) { ret.push(subs[i]); } else { ret.push(cmd); }
 
++i;
 
} else {
 
var j=parseInt( cmd.replace('$', ''), 10 ) - 1;
 
if ( j > -1 && j < subs.length ) { ret.push(subs[j]); } else { ret.push(cmd); }
 
 
}
 
}
} while (s.length > 0);
+
var ret = [];
return ret.join('');
+
var s = str.parenSplit(/(%s|\$[0-9]+)/);
}
+
var i = 0;
 
+
do {
function max(a,b){return a<b ? b : a;}
+
ret.push(s.shift());
function min(a,b){return a>b ? b : a;}
+
if (!s.length) {
 
+
break;
function isString(x) { return (typeof x === 'string' || x instanceof String); }
 
//function isNumber(x) { return (typeof x === 'number' || x instanceof Number); }
 
function isRegExp(x) { return x instanceof RegExp; }
 
function isArray (x) { return x instanceof Array; }
 
function isObject(x) { return x instanceof Object; }
 
function isFunction(x) {
 
return !isRegExp(x) && ($.isFunction(x) || x instanceof Function);
 
}
 
 
 
function repeatString(s,mult) {
 
var ret='';
 
for (var i=0; i<mult; ++i) { ret += s; }
 
return ret;
 
}
 
 
 
function zeroFill(s, min) {
 
min = min || 2;
 
var t=s.toString();
 
return repeatString('0', min - t.length) + t;
 
}
 
 
 
function map(f, o) {
 
if (isArray(o)) { return map_array(f,o); }
 
return map_object(f,o);
 
}
 
function map_array(f,o) {
 
var ret=[];
 
for (var i=0; i<o.length; ++i) {
 
ret.push(f(o[i]));
 
}
 
return ret;
 
}
 
function map_object(f,o) {
 
var ret={};
 
for (var i in o) { ret[o]=f(o[i]); }
 
return ret;
 
}
 
 
 
pg.escapeQuotesHTML = function ( text ) {
 
return text
 
.replace(/&/g, "&amp;")
 
.replace(/"/g, "&quot;")
 
.replace(/</g, "&lt;")
 
.replace(/>/g, "&gt;");
 
};
 
 
 
pg.jsescape = function(s)
 
{
 
if (typeof s !== "string") throw "Invalid type in pg.jsescape";
 
var res = "";
 
//this can be optimized by copying substrings instead of char by char!
 
for (var i=0; i<s.length; i++)
 
{
 
var c = s[i];
 
switch (c)
 
{
 
  case '\b': res += '\\b'; continue;
 
  case '\f': res += '\\f'; continue;
 
  case '\n': res += '\\n'; continue;
 
  case '\0': res += '\\0'; continue;
 
  case '\r': res += '\\r'; continue;
 
  case '\t': res += '\\t'; continue;
 
  case '\v': res += '\\v'; continue;
 
  case '\\': res += '\\\\'; continue;
 
  case '\"':  res += '\\\"'; continue;
 
  case '\'':  res += '\\\''; continue;
 
  default:
 
if (c < ' ' || c==='<' || c==='>' || c==="'")
 
{
 
var unicodeChar = c.charCodeAt(0).toString(16).toUpperCase();
 
res += "\\u" + (unicodeChar.length>1?"00":"000") + unicodeChar;
 
 
}
 
}
else
+
var cmd = s.shift();
{
+
if (cmd == '%s') {
res += c;
+
if (i < subs.length) {
 +
ret.push(subs[i]);
 +
} else {
 +
ret.push(cmd);
 +
}
 +
++i;
 +
} else {
 +
var j = parseInt(cmd.replace('$', ''), 10) - 1;
 +
if (j > -1 && j < subs.length) {
 +
ret.push(subs[j]);
 +
} else {
 +
ret.push(cmd);
 +
}
 
}
 
}
 +
} while (s.length > 0);
 +
return ret.join('');
 
}
 
}
  }
 
  return res;
 
};
 
// ENDFILE: tools.js
 
// STARTFILE: dab.js
 
//<NOLITE>
 
//////////////////////////////////////////////////
 
// Dab-fixing code
 
//
 
  
 +
/* eslint-disable no-unused-vars */
 +
function isString(x) {
 +
return typeof x === 'string' || x instanceof String;
 +
}
  
function retargetDab(newTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) {
+
function isNumber(x) {
log('retargetDab: newTarget='+newTarget + ' oldTarget=' + oldTarget);
+
return typeof x === 'number' || x instanceof Number;
return changeLinkTargetLink(
+
}
{newTarget: newTarget,
 
text: newTarget.split(' ').join('&nbsp;'),
 
hint: tprintf('disambigHint', [newTarget]),
 
summary: simplePrintf(
 
getValueOf('popupFixDabsSummary'), [friendlyCurrentArticleName, newTarget ]),
 
clickButton: getValueOf('popupDabsAutoClick'), minor: true, oldTarget: oldTarget,
 
watch: getValueOf('popupWatchDisambiggedPages'),
 
title: titleToEdit});
 
}
 
  
function listLinks(wikitext, oldTarget, titleToEdit) {
+
function isRegExp(x) {
// mediawiki strips trailing spaces, so we do the same
+
return x instanceof RegExp;
// testcase: http://en.wikipedia.org/w/index.php?title=Radial&oldid=97365633
+
}
var reg=RegExp('\\[\\[([^|]*?) *(\\||\\]\\])', 'gi');
 
var ret=[];
 
var splitted=wikitext.parenSplit(reg);
 
// ^[a-z]+ should match interwiki links, hopefully (case-insensitive)
 
// and ^[a-z]* should match those and [[:Category...]] style links too
 
var omitRegex=RegExp('^[a-z]*:|^[Ss]pecial:|^[Ii]mage|^[Cc]ategory');
 
var friendlyCurrentArticleName= oldTarget.toString();
 
var wikPos = getValueOf('popupDabWiktionary');
 
  
for (var i=1; i<splitted.length; i=i+3) {
+
function isArray(x) {
if (typeof splitted[i] == typeof 'string' && splitted[i].length>0 && !omitRegex.test(splitted[i])) {
+
return x instanceof Array;
ret.push( retargetDab(splitted[i], oldTarget, friendlyCurrentArticleName, titleToEdit) );
+
}
} /* if */
 
} /* for loop */
 
  
ret = rmDupesFromSortedList(ret.sort());
+
function isObject(x) {
 +
return x instanceof Object;
 +
}
  
if (wikPos) {
+
function isFunction(x) {
var wikTarget='wiktionary:' +
+
return !isRegExp(x) && (typeof x === 'function' || x instanceof Function);
friendlyCurrentArticleName.replace( RegExp('^(.+)\\s+[(][^)]+[)]\\s*$'), '$1' );
+
}
 +
/* eslint-enable no-unused-vars */
  
var meth;
+
function repeatString(s, mult) {
if (wikPos.toLowerCase() == 'first') { meth = 'unshift'; }
+
var ret = '';
else { meth = 'push'; }
+
for (var i = 0; i < mult; ++i) {
 
+
ret += s;
ret[meth]( retargetDab(wikTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) );
+
}
 +
return ret;
 
}
 
}
  
ret.push(changeLinkTargetLink(
+
function zeroFill(s, min) {
{ newTarget: null,
+
min = min || 2;
text: popupString('remove this link').split(' ').join('&nbsp;'),
+
var t = s.toString();
hint: popupString("remove all links to this disambig page from this article"),
+
return repeatString('0', min - t.length) + t;
clickButton: "wpDiff", oldTarget: oldTarget,
 
summary: simplePrintf(getValueOf('popupRmDabLinkSummary'), [friendlyCurrentArticleName]),
 
watch: getValueOf('popupWatchDisambiggedPages'),
 
title: titleToEdit
 
}));
 
return ret;
 
}
 
 
 
function rmDupesFromSortedList(list) {
 
var ret=[];
 
for (var i=0; i<list.length; ++i) {
 
if (ret.length === 0 || list[i]!=ret[ret.length-1]) { ret.push(list[i]); }
 
 
}
 
}
return ret;
 
}
 
  
function makeFixDab(data, navpop) {
+
function map(f, o) {
// grab title from parent popup if there is one; default exists in changeLinkTargetLink
+
if (isArray(o)) {
var titleToEdit=(navpop.parentPopup && navpop.parentPopup.article.toString());
+
return map_array(f, o);
var list=listLinks(data, navpop.originalArticle, titleToEdit);
+
}
if (list.length === 0) { log('listLinks returned empty list'); return null; }
+
return map_object(f, o);
var html='<hr />' + popupString('Click to disambiguate this link to:') + '<br>';
+
}
html+=list.join(', ');
+
function map_array(f, o) {
return html;
+
var ret = [];
}
+
for (var i = 0; i < o.length; ++i) {
 +
ret.push(f(o[i]));
 +
}
 +
return ret;
 +
}
 +
function map_object(f, o) {
 +
var ret = {};
 +
for (var i in o) {
 +
ret[o] = f(o[i]);
 +
}
 +
return ret;
 +
}
  
 +
pg.escapeQuotesHTML = function (text) {
 +
return text
 +
.replace(/&/g, '&amp;')
 +
.replace(/"/g, '&quot;')
 +
.replace(/</g, '&lt;')
 +
.replace(/>/g, '&gt;');
 +
};
  
function makeFixDabs(wikiText, navpop) {
+
pg.unescapeQuotesHTML = function (html) {
if (getValueOf('popupFixDabs') && isDisambig(wikiText, navpop.article) &&
+
// From https://stackoverflow.com/a/7394787
Title.fromURL(location.href).namespaceId() != pg.nsSpecialId &&
+
// This seems to be implemented correctly on all major browsers now, so we
navpop.article.talkPage() ) {
+
// don't have to make our own function.
setPopupHTML(makeFixDab(wikiText, navpop), 'popupFixDab', navpop.idNumber);
+
var txt = document.createElement('textarea');
}
+
txt.innerHTML = html;
}
+
return txt.value;
 +
};
  
function popupRedlinkHTML(article) {
+
// ENDFILE: tools.js
return changeLinkTargetLink(
 
{ newTarget: null, text: popupString('remove this link').split(' ').join('&nbsp;'),
 
hint: popupString("remove all links to this page from this article"),
 
clickButton: "wpDiff",
 
oldTarget: article.toString(),
 
summary: simplePrintf(getValueOf('popupRedlinkSummary'), [article.toString()])});
 
}
 
//</NOLITE>
 
// ENDFILE: dab.js
 
// STARTFILE: htmloutput.js
 
  
function appendPopupContent(obj, elementId, popupId, onSuccess) {
+
// STARTFILE: dab.js
return setPopupHTML(obj, elementId, popupId, onSuccess, true);
+
//<NOLITE>
}
+
//////////////////////////////////////////////////
 +
// Dab-fixing code
 +
//
  
// this has to use a timer loop as we don't know if the DOM element exists when we want to set the text
+
function retargetDab(newTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) {
function setPopupHTML (str, elementId, popupId, onSuccess, append) {
+
log('retargetDab: newTarget=' + newTarget + ' oldTarget=' + oldTarget);
if (elementId=='popupPreview') {
+
return changeLinkTargetLink({
}
+
newTarget: newTarget,
if (typeof popupId === 'undefined') {
+
text: newTarget.split(' ').join('&nbsp;'),
//console.error('popupId is not defined in setPopupHTML, html='+str.substring(0,100));
+
hint: tprintf('disambigHint', [newTarget]),
popupId = pg.idNumber;
+
summary: simplePrintf(getValueOf('popupFixDabsSummary'), [
 +
friendlyCurrentArticleName,
 +
newTarget,
 +
]),
 +
clickButton: getValueOf('popupDabsAutoClick'),
 +
minor: true,
 +
oldTarget: oldTarget,
 +
watch: getValueOf('popupWatchDisambiggedPages'),
 +
title: titleToEdit,
 +
});
 
}
 
}
  
var popupElement=document.getElementById(elementId+popupId);
+
function listLinks(wikitext, oldTarget, titleToEdit) {
if (popupElement) {
+
// mediawiki strips trailing spaces, so we do the same
if (!append) { popupElement.innerHTML=''; }
+
// testcase: https://en.wikipedia.org/w/index.php?title=Radial&oldid=97365633
if (isString(str)) {
+
var reg = RegExp('\\[\\[([^|]*?) *(\\||\\]\\])', 'gi');
popupElement.innerHTML+=str;
+
var ret = [];
} else {
+
var splitted = wikitext.parenSplit(reg);
popupElement.appendChild(str);
+
// ^[a-z]+ should match interwiki links, hopefully (case-insensitive)
}
+
// and ^[a-z]* should match those and [[:Category...]] style links too
if (onSuccess) { onSuccess(); }
+
var omitRegex = RegExp('^[a-z]*:|^[Ss]pecial:|^[Ii]mage|^[Cc]ategory');
setTimeout(checkPopupPosition, 100);
+
var friendlyCurrentArticleName = oldTarget.toString();
return true;
+
var wikPos = getValueOf('popupDabWiktionary');
} else {
+
 
// call this function again in a little while...
+
for (var i = 1; i < splitted.length; i = i + 3) {
setTimeout(function(){
+
if (
setPopupHTML(str,elementId,popupId,onSuccess);
+
typeof splitted[i] == typeof 'string' &&
}, 600);
+
splitted[i].length > 0 &&
}
+
!omitRegex.test(splitted[i])
return null;
+
) {
}
+
ret.push(retargetDab(splitted[i], oldTarget, friendlyCurrentArticleName, titleToEdit));
 +
} /* if */
 +
} /* for loop */
  
//<NOLITE>
+
ret = rmDupesFromSortedList(ret.sort());
function setPopupTrailer(str,id) {return setPopupHTML(str, 'popupData', id);}
 
//</NOLITE>
 
  
 +
if (wikPos) {
 +
var wikTarget =
 +
'wiktionary:' +
 +
friendlyCurrentArticleName.replace(RegExp('^(.+)\\s+[(][^)]+[)]\\s*$'), '$1');
  
function fillEmptySpans(args) { return fillEmptySpans2(args); }
+
var meth;
 +
if (wikPos.toLowerCase() == 'first') {
 +
meth = 'unshift';
 +
} else {
 +
meth = 'push';
 +
}
  
// args.navpopup is mandatory
+
ret[meth](retargetDab(wikTarget, oldTarget, friendlyCurrentArticleName, titleToEdit));
// optional: args.redir, args.redirTarget
+
}
// FIXME: ye gods, this is ugly stuff
 
function fillEmptySpans2(args) { // if redir is present and true then redirTarget is mandatory
 
var redir=true;
 
if (typeof args != 'object' || typeof args.redir == 'undefined' || !args.redir) { redir=false; }
 
var a=args.navpopup.parentAnchor;
 
  
var article, hint=null, oldid=null, params={};
+
ret.push(
if (redir && typeof args.redirTarget == typeof {}) {
+
changeLinkTargetLink({
article=args.redirTarget;
+
newTarget: null,
//hint=article.hintValue();
+
text: popupString('remove this link').split(' ').join('&nbsp;'),
} else {
+
hint: popupString('remove all links to this disambig page from this article'),
article=(new Title()).fromAnchor(a);
+
clickButton: getValueOf('popupDabsAutoClick'),
hint=a.originalTitle || article.hintValue();
+
oldTarget: oldTarget,
params=parseParams(a.href);
+
summary: simplePrintf(getValueOf('popupRmDabLinkSummary'), [friendlyCurrentArticleName]),
oldid=(getValueOf('popupHistoricalLinks')) ? params.oldid : null;
+
watch: getValueOf('popupWatchDisambiggedPages'),
rcid=params.rcid;
+
title: titleToEdit,
 +
})
 +
);
 +
return ret;
 
}
 
}
var x={ article:article, hint: hint, oldid: oldid, rcid: rcid, navpop:args.navpopup, params:params };
 
  
var structure=pg.structures[getValueOf('popupStructure')];
+
function rmDupesFromSortedList(list) {
if (typeof structure != 'object') {
+
var ret = [];
setPopupHTML('popupError', 'Unknown structure (this should never happen): '+
+
for (var i = 0; i < list.length; ++i) {
pg.option.popupStructure, args.navpopup.idNumber);
+
if (ret.length === 0 || list[i] != ret[ret.length - 1]) {
return;
+
ret.push(list[i]);
 +
}
 +
}
 +
return ret;
 
}
 
}
var spans=flatten(pg.misc.layout);
 
var numspans = spans.length;
 
var redirs=pg.misc.redirSpans;
 
  
for (var i=0; i<numspans; ++i) {
+
function makeFixDab(data, navpop) {
var f=findThis(redirs, spans[i]);
+
// grab title from parent popup if there is one; default exists in changeLinkTargetLink
//log('redir='+redir+', f='+f+', spans[i]='+spans[i]);
+
var titleToEdit = navpop.parentPopup && navpop.parentPopup.article.toString();
if ( (f && !redir) || (!f && redir) ) {
+
var list = listLinks(data, navpop.originalArticle, titleToEdit);
//log('skipping this set of the loop');
+
if (list.length === 0) {
continue;
+
log('listLinks returned empty list');
}
+
return null;
var structurefn=structure[spans[i]];
 
var setfn = setPopupHTML;
 
if (getValueOf('popupActiveNavlinks') &&  
 
(spans[i].indexOf('popupTopLinks')===0 || spans[i].indexOf('popupRedirTopLinks')===0)
 
) {
 
setfn = setPopupTipsAndHTML;
 
}
 
switch (typeof structurefn) {
 
case 'function':
 
log('running '+spans[i]+'({article:'+x.article+', hint:'+x.hint+', oldid: '+x.oldid+'})');
 
setfn(structurefn(x), spans[i], args.navpopup.idNumber);
 
break;
 
case 'string':
 
setfn(structurefn, spans[i], args.navpopup.idNumber);
 
break;
 
default:
 
errlog('unknown thing with label '+spans[i] + ' (span index was ' + i + ')');
 
break;
 
 
}
 
}
 +
var html = '<hr />' + popupString('Click to disambiguate this link to:') + '<br>';
 +
html += list.join(', ');
 +
return html;
 
}
 
}
}
 
  
// flatten an array
+
function makeFixDabs(wikiText, navpop) {
function flatten(list, start) {
+
if (
var ret=[];
+
getValueOf('popupFixDabs') &&
if (typeof start == 'undefined') { start=0; }
+
isDisambig(wikiText, navpop.article) &&
for (var i=start; i<list.length; ++i) {
+
Title.fromURL(location.href).namespaceId() != pg.nsSpecialId &&
if (typeof list[i] == typeof []) {
+
navpop.article.talkPage()
return ret.concat(flatten(list[i])).concat(flatten(list, i+1));
+
) {
 +
setPopupHTML(makeFixDab(wikiText, navpop), 'popupFixDab', navpop.idNumber);
 
}
 
}
else { ret.push(list[i]); }
 
 
}
 
}
return ret;
 
}
 
  
// Generate html for whole popup
+
function popupRedlinkHTML(article) {
function popupHTML (a) {
+
return changeLinkTargetLink({
getValueOf('popupStructure');
+
newTarget: null,
var structure=pg.structures[pg.option.popupStructure];
+
text: popupString('remove this link').split(' ').join('&nbsp;'),
if (typeof structure != 'object') {
+
hint: popupString('remove all links to this page from this article'),
//return 'Unknown structure: '+pg.option.popupStructure;
+
clickButton: getValueOf('popupRedlinkAutoClick'),
// override user choice
+
oldTarget: article.toString(),
pg.option.popupStructure=pg.optionDefault.popupStructure;
+
summary: simplePrintf(getValueOf('popupRedlinkSummary'), [article.toString()]),
return popupHTML(a);
+
});
 
}
 
}
if (typeof structure.popupLayout != 'function') { return 'Bad layout'; }
+
//</NOLITE>
pg.misc.layout=structure.popupLayout();
+
// ENDFILE: dab.js
if ($.isFunction(structure.popupRedirSpans)) { pg.misc.redirSpans=structure.popupRedirSpans(); }
+
 
else { pg.misc.redirSpans=[]; }
+
// STARTFILE: htmloutput.js
return makeEmptySpans(pg.misc.layout, a.navpopup);
+
 
}
+
// this has to use a timer loop as we don't know if the DOM element exists when we want to set the text
 +
function setPopupHTML(str, elementId, popupId, onSuccess, append) {
 +
if (typeof popupId === 'undefined') {
 +
//console.error('popupId is not defined in setPopupHTML, html='+str.substring(0,100));
 +
popupId = pg.idNumber;
 +
}
  
function makeEmptySpans (list, navpop) {
+
var popupElement = document.getElementById(elementId + popupId);
var ret='';
+
if (popupElement) {
for (var i=0; i<list.length; ++i) {
+
if (!append) {
if (typeof list[i] == typeof '') {
+
popupElement.innerHTML = '';
ret += emptySpanHTML(list[i], navpop.idNumber, 'div');
+
}
} else if (typeof list[i] == typeof [] && list[i].length > 0 ) {
+
if (isString(str)) {
ret = ret.parenSplit(RegExp('(</[^>]*?>$)')).join(makeEmptySpans(list[i], navpop));
+
popupElement.innerHTML += str;
} else if (typeof list[i] == typeof {} && list[i].nodeType ) {
+
} else {
ret += emptySpanHTML(list[i].name, navpop.idNumber, list[i].nodeType);
+
popupElement.appendChild(str);
 +
}
 +
if (onSuccess) {
 +
onSuccess();
 +
}
 +
setTimeout(checkPopupPosition, 100);
 +
return true;
 +
} else {
 +
// call this function again in a little while...
 +
setTimeout(function () {
 +
setPopupHTML(str, elementId, popupId, onSuccess);
 +
}, 600);
 
}
 
}
 +
return null;
 
}
 
}
return ret;
 
}
 
  
 +
//<NOLITE>
 +
function setPopupTrailer(str, id) {
 +
return setPopupHTML(str, 'popupData', id);
 +
}
 +
//</NOLITE>
  
function emptySpanHTML(name, id, tag, classname) {
+
// args.navpopup is mandatory
tag = tag || 'span';
+
// optional: args.redir, args.redirTarget
if (!classname) { classname = emptySpanHTML.classAliases[name]; }
+
// FIXME: ye gods, this is ugly stuff
classname = classname || name;
+
function fillEmptySpans(args) {
if (name == getValueOf('popupDragHandle')) { classname += ' popupDragHandle'; }
+
// if redir is present and true then redirTarget is mandatory
return simplePrintf('<%s id="%s" class="%s"></%s>', [tag, name + id, classname, tag]);
+
var redir = true;
}
+
var rcid;
emptySpanHTML.classAliases={ 'popupSecondPreview': 'popupPreview' };
+
if (typeof args != 'object' || typeof args.redir == 'undefined' || !args.redir) {
 +
redir = false;
 +
}
 +
var a = args.navpopup.parentAnchor;
  
// generate html for popup image
+
var article,
// <a id="popupImageLinkn"><img id="popupImagen">
+
hint = null,
// where n=idNumber
+
oldid = null,
function imageHTML(article, idNumber) {
+
params = {};
return simplePrintf('<a id="popupImageLink$1">' +
+
if (redir && typeof args.redirTarget == typeof {}) {
'<img align="right" valign="top" id="popupImg$1" style="display: none;"></img>' +
+
article = args.redirTarget;
'</a>', [ idNumber ]);
+
//hint=article.hintValue();
}
+
} else {
 +
article = new Title().fromAnchor(a);
 +
hint = a.originalTitle || article.hintValue();
 +
params = parseParams(a.href);
 +
oldid = getValueOf('popupHistoricalLinks') ? params.oldid : null;
 +
rcid = params.rcid;
 +
}
 +
var x = {
 +
article: article,
 +
hint: hint,
 +
oldid: oldid,
 +
rcid: rcid,
 +
navpop: args.navpopup,
 +
params: params,
 +
};
  
function popTipsSoonFn(id, when, popData) {
+
var structure = pg.structures[getValueOf('popupStructure')];
if (!when) { when=250; }
+
if (typeof structure != 'object') {
var popTips=function(){ setupTooltips(document.getElementById(id), false, true, popData); };
+
setPopupHTML(
return function() { setTimeout( popTips, when, popData ); };
+
'popupError',
}
+
'Unknown structure (this should never happen): ' + pg.option.popupStructure,
 +
args.navpopup.idNumber
 +
);
 +
return;
 +
}
 +
var spans = flatten(pg.misc.layout);
 +
var numspans = spans.length;
 +
var redirs = pg.misc.redirSpans;
  
function setPopupTipsAndHTML(html, divname, idnumber, popData) {
+
for (var i = 0; i < numspans; ++i) {
setPopupHTML(html, divname, idnumber,
+
var found = redirs && redirs.indexOf(spans[i]) !== -1;
getValueOf('popupSubpopups') ?
+
//log('redir='+redir+', found='+found+', spans[i]='+spans[i]);
popTipsSoonFn(divname + idnumber, null, popData) :
+
if ((found && !redir) || (!found && redir)) {
null);
+
//log('skipping this set of the loop');
}
+
continue;
// ENDFILE: htmloutput.js
+
}
// STARTFILE: mouseout.js
+
var structurefn = structure[spans[i]];
//////////////////////////////////////////////////
+
if (structurefn === undefined) {
// fuzzy checks
+
// nothing to do for this structure part
 
+
continue;
function fuzzyCursorOffMenus(x,y, fuzz, parent) {
+
}
if (!parent) { return null; }
+
var setfn = setPopupHTML;
var uls=parent.getElementsByTagName('ul');
+
if (
for (var i=0; i<uls.length; ++i) {
+
getValueOf('popupActiveNavlinks') &&
if (uls[i].className=='popup_menu') {
+
(spans[i].indexOf('popupTopLinks') === 0 || spans[i].indexOf('popupRedirTopLinks') === 0)
if (uls[i].offsetWidth > 0) return false;
+
) {
} // else {document.title+='.';}
+
setfn = setPopupTipsAndHTML;
 +
}
 +
switch (typeof structurefn) {
 +
case 'function':
 +
log(
 +
'running ' +
 +
spans[i] +
 +
'({article:' +
 +
x.article +
 +
', hint:' +
 +
x.hint +
 +
', oldid: ' +
 +
x.oldid +
 +
'})'
 +
);
 +
setfn(structurefn(x), spans[i], args.navpopup.idNumber);
 +
break;
 +
case 'string':
 +
setfn(structurefn, spans[i], args.navpopup.idNumber);
 +
break;
 +
default:
 +
errlog('unknown thing with label ' + spans[i] + ' (span index was ' + i + ')');
 +
break;
 +
}
 +
}
 
}
 
}
return true;
 
}
 
  
function checkPopupPosition () { // stop the popup running off the right of the screen
+
// flatten an array
// FIXME avoid pg.current.link
+
function flatten(list, start) {
if (pg.current.link && pg.current.link.navpopup)
+
var ret = [];
pg.current.link.navpopup.limitHorizontalPosition();
+
if (typeof start == 'undefined') {
}
+
start = 0;
 
+
}
function mouseOutWikiLink () {
+
for (var i = start; i < list.length; ++i) {
//console ('mouseOutWikiLink');
+
if (typeof list[i] == typeof []) {
var a=this;
+
return ret.concat(flatten(list[i])).concat(flatten(list, i + 1));
if (a.navpopup === null || typeof a.navpopup === 'undefined') return;
+
} else {
if ( ! a.navpopup.isVisible() ) {
+
ret.push(list[i]);
a.navpopup.banish();
+
}
return;
+
}
 +
return ret;
 
}
 
}
restoreTitle(a);
 
Navpopup.tracker.addHook(posCheckerHook(a.navpopup));
 
}
 
  
function posCheckerHook(navpop) {
+
// Generate html for whole popup
return function() {
+
function popupHTML(a) {
if (!navpop.isVisible()) { return true; /* remove this hook */ }
+
getValueOf('popupStructure');
if (Navpopup.tracker.dirty) {
+
var structure = pg.structures[pg.option.popupStructure];
return false;
+
if (typeof structure != 'object') {
 +
//return 'Unknown structure: '+pg.option.popupStructure;
 +
// override user choice
 +
pg.option.popupStructure = pg.optionDefault.popupStructure;
 +
return popupHTML(a);
 +
}
 +
if (typeof structure.popupLayout != 'function') {
 +
return 'Bad layout';
 +
}
 +
pg.misc.layout = structure.popupLayout();
 +
if (typeof structure.popupRedirSpans === 'function') {
 +
pg.misc.redirSpans = structure.popupRedirSpans();
 +
} else {
 +
pg.misc.redirSpans = [];
 
}
 
}
var x=Navpopup.tracker.x, y=Navpopup.tracker.y;
+
return makeEmptySpans(pg.misc.layout, a.navpopup);
var mouseOverNavpop = navpop.isWithin(x,y,navpop.fuzz, navpop.mainDiv) ||
+
}
!fuzzyCursorOffMenus(x,y,navpop.fuzz, navpop.mainDiv);
 
  
// FIXME it'd be prettier to do this internal to the Navpopup objects
+
function makeEmptySpans(list, navpop) {
var t=getValueOf('popupHideDelay');
+
var ret = '';
if (t) { t = t * 1000; }
+
for (var i = 0; i < list.length; ++i) {
if (!t) {
+
if (typeof list[i] == typeof '') {
if(!mouseOverNavpop) {
+
ret += emptySpanHTML(list[i], navpop.idNumber, 'div');
if(navpop.parentAnchor) {
+
} else if (typeof list[i] == typeof [] && list[i].length > 0) {
restoreTitle( navpop.parentAnchor );
+
ret = ret.parenSplit(RegExp('(</[^>]*?>$)')).join(makeEmptySpans(list[i], navpop));
}
+
} else if (typeof list[i] == typeof {} && list[i].nodeType) {
navpop.banish();
+
ret += emptySpanHTML(list[i].name, navpop.idNumber, list[i].nodeType);
return true; /* remove this hook */
 
 
}
 
}
return false;
 
 
}
 
}
// we have a hide delay set
+
return ret;
var d=+(new Date());
+
}
if ( !navpop.mouseLeavingTime ) {
+
 
navpop.mouseLeavingTime = d;
+
function emptySpanHTML(name, id, tag, classname) {
return false;
+
tag = tag || 'span';
}
+
if (!classname) {
if ( mouseOverNavpop ) {
+
classname = emptySpanHTML.classAliases[name];
navpop.mouseLeavingTime=null;
 
return false;
 
 
}
 
}
if (d - navpop.mouseLeavingTime > t) {
+
classname = classname || name;
navpop.mouseLeavingTime=null;
+
if (name == getValueOf('popupDragHandle')) {
navpop.banish(); return true; /* remove this hook */
+
classname += ' popupDragHandle';
 
}
 
}
return false;
+
return simplePrintf('<%s id="%s" class="%s"></%s>', [tag, name + id, classname, tag]);
};
+
}
}
+
emptySpanHTML.classAliases = { popupSecondPreview: 'popupPreview' };
  
function runStopPopupTimer(navpop) {
+
// generate html for popup image
// at this point, we should have left the link but remain within the popup
+
// <a id="popupImageLinkn"><img id="popupImagen">
// so we call this function again until we leave the popup.
+
// where n=idNumber
if (!navpop.stopPopupTimer) {
+
function imageHTML(article, idNumber) {
navpop.stopPopupTimer=setInterval(posCheckerHook(navpop), 500);
+
return simplePrintf(
navpop.addHook(function(){clearInterval(navpop.stopPopupTimer);},
+
'<a id="popupImageLink$1">' +
  'hide', 'before');
+
'<img align="right" valign="top" id="popupImg$1" style="display: none;"></img>' +
 +
'</a>',
 +
[idNumber]
 +
);
 
}
 
}
}
 
// ENDFILE: mouseout.js
 
// STARTFILE: previewmaker.js
 
/**
 
  @fileoverview
 
  Defines the {@link Previewmaker} object, which generates short previews from wiki markup.
 
*/
 
  
/**
+
function popTipsSoonFn(id, when, popData) {
  Creates a new Previewmaker
+
if (!when) {
  @constructor
+
when = 250;
  @class The Previewmaker class. Use an instance of this to generate short previews from Wikitext.
 
  @param {String} wikiText The Wikitext source of the page we wish to preview.
 
  @param {String} baseUrl The url we should prepend when creating relative urls.
 
  @param {Navpopup} owner The navpop associated to this preview generator
 
*/
 
function Previewmaker(wikiText, baseUrl, owner) {
 
/** The wikitext which is manipulated to generate the preview. */
 
this.originalData=wikiText;
 
this.setData();
 
this.baseUrl=baseUrl;
 
this.owner=owner;
 
this.maxCharacters=getValueOf('popupMaxPreviewCharacters');
 
this.maxSentences=getValueOf('popupMaxPreviewSentences');
 
}
 
Previewmaker.prototype.setData=function() {
 
var maxSize=max(10000, 2*this.maxCharacters);
 
this.data=this.originalData.substring(0,maxSize);
 
};
 
/** Remove HTML comments
 
@private
 
*/
 
Previewmaker.prototype.killComments = function () {
 
// this also kills one trailing newline, eg [[diamyo]]
 
this.data=this.data.replace(RegExp('^<!--[^$]*?-->\\n|\\n<!--[^$]*?-->(?=\\n)|<!--[^$]*?-->', 'g'), '');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killDivs = function () {
 
// say goodbye, divs (can be nested, so use * not *?)
 
this.data=this.data.replace(RegExp('< *div[^>]* *>[\\s\\S]*?< */ *div *>',
 
  'gi'), '');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killGalleries = function () {
 
this.data=this.data.replace(RegExp('< *gallery[^>]* *>[\\s\\S]*?< */ *gallery *>',
 
  'gi'), '');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.kill = function(opening, closing, subopening, subclosing, repl) {
 
var oldk=this.data;
 
var k=this.killStuff(this.data, opening, closing, subopening, subclosing, repl);
 
while (k.length < oldk.length) {
 
oldk=k;
 
k=this.killStuff(k, opening, closing, subopening, subclosing, repl);
 
}
 
this.data=k;
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killStuff = function (txt, opening, closing, subopening, subclosing, repl) {
 
var op=this.makeRegexp(opening);
 
var cl=this.makeRegexp(closing, '^');
 
var sb=subopening ? this.makeRegexp(subopening, '^') : null;
 
var sc=subclosing ? this.makeRegexp(subclosing, '^') : cl;
 
if (!op || !cl) {
 
alert('Navigation Popups error: op or cl is null! something is wrong.');
 
return;
 
}
 
if (!op.test(txt)) { return txt; }
 
var ret='';
 
var opResult = op.exec(txt);
 
ret = txt.substring(0,opResult.index);
 
txt=txt.substring(opResult.index+opResult[0].length);
 
var depth = 1;
 
while (txt.length > 0) {
 
var removal=0;
 
if (depth==1 && cl.test(txt)) {
 
depth--;
 
removal=cl.exec(txt)[0].length;
 
} else if (depth > 1 && sc.test(txt)) {
 
depth--;
 
removal=sc.exec(txt)[0].length;
 
}else if (sb && sb.test(txt)) {
 
depth++;
 
removal=sb.exec(txt)[0].length;
 
 
}
 
}
if ( !removal ) { removal = 1; }
+
var popTips = function () {
txt=txt.substring(removal);
+
setupTooltips(document.getElementById(id), false, true, popData);
if (depth === 0) { break; }
+
};
 +
return function () {
 +
setTimeout(popTips, when, popData);
 +
};
 
}
 
}
return ret + (repl || '') + txt;
+
 
};
+
function setPopupTipsAndHTML(html, divname, idnumber, popData) {
/**
+
setPopupHTML(
  @private
+
html,
*/
+
divname,
Previewmaker.prototype.makeRegexp = function (x, prefix, suffix) {
+
idnumber,
prefix = prefix || '';
+
getValueOf('popupSubpopups') ? popTipsSoonFn(divname + idnumber, null, popData) : null
suffix = suffix || '';
+
);
var reStr='';
 
var flags='';
 
if (isString(x)) {
 
reStr=prefix + literalizeRegex(x) + suffix;
 
} else if (isRegExp(x)) {
 
var s=x.toString().substring(1);
 
var sp=s.split('/');
 
flags=sp[sp.length-1];
 
sp[sp.length-1]='';
 
s=sp.join('/');
 
s=s.substring(0,s.length-1);
 
reStr= prefix + s + suffix;
 
} else {
 
log ('makeRegexp failed');
 
 
}
 
}
 +
// ENDFILE: htmloutput.js
  
log ('makeRegexp: got reStr=' + reStr + ', flags=' + flags);
+
// STARTFILE: mouseout.js
return RegExp(reStr, flags);
+
//////////////////////////////////////////////////
};
+
// fuzzy checks
/**
 
  @private
 
*/
 
Previewmaker.prototype.killBoxTemplates = function () {
 
  
// taxobox removal... in fact, there's a saudiprincebox_begin, so let's be more general
+
function fuzzyCursorOffMenus(x, y, fuzz, parent) {
// also, have float_begin, ... float_end
+
if (!parent) {
this.kill(RegExp('[{][{][^{}\\s|]*?(float|box)[_ ](begin|start)', 'i'), /[}][}]\s*/, '{{');
+
return null;
 
+
}
// infoboxes etc
+
var uls = parent.getElementsByTagName('ul');
// from [[User:Zyxw/popups.js]]: kill frames too
+
for (var i = 0; i < uls.length; ++i) {
this.kill(RegExp('[{][{][^{}\\s|]*?(infobox|elementbox|frame)[_ ]', 'i'), /[}][}]\s*/, '{{');
+
if (uls[i].className == 'popup_menu') {
 
+
if (uls[i].offsetWidth > 0) return false;
};
+
} // else {document.title+='.';}
/**
 
  @private
 
*/
 
Previewmaker.prototype.killTemplates = function () {
 
this.kill('{{', '}}', '{', '}', ' ');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killTables = function () {
 
// tables are bad, too
 
// this can be slow, but it's an inprovement over a browser hang
 
// torture test: [[Comparison_of_Intel_Central_Processing_Units]]
 
this.kill('{|', /[|]}\s*/, '{|');
 
this.kill(/<table.*?>/i, /<\/table.*?>/i, /<table.*?>/i);
 
// remove lines starting with a pipe for the hell of it (?)
 
this.data=this.data.replace(RegExp('^[|].*$', 'mg'), '');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killImages = function () {
 
var forbiddenNamespaceAliases = [];
 
jQuery.each(mw.config.get('wgNamespaceIds'), function(_localizedNamespaceLc, _namespaceId) {
 
if (_namespaceId!=pg.nsImageId && _namespaceId!=pg.nsCategoryId) return;
 
forbiddenNamespaceAliases.push(_localizedNamespaceLc.split(' ').join('[ _]')); //todo: escape regexp fragments!
 
});
 
 
// images and categories are a nono
 
this.kill(RegExp('[[][[]\\s*(' + forbiddenNamespaceAliases.join('|') + ')\\s*:', 'i'),
 
  /\]\]\s*/, '[', ']');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killHTML = function () {
 
// kill <ref ...>...</ref>
 
this.kill(/<ref\b[^/>]*?>/i, /<\/ref>/i);
 
 
 
// let's also delete entire lines starting with <. it's worth a try.
 
this.data=this.data.replace(RegExp('(^|\\n) *<.*', 'g'), '\n');
 
 
 
// and those pesky html tags, but not <nowiki> or <blockquote>
 
var splitted=this.data.parenSplit(/(<[\w\W]*?(?:>|$|(?=<)))/);
 
var len=splitted.length;
 
for (var i=1; i<len; i=i+2) {
 
switch (splitted[i]) {
 
case '<nowiki>':
 
case '</nowiki>':
 
case '<blockquote>':
 
case '</blockquote>':
 
break;
 
default:
 
splitted[i]='';
 
 
}
 
}
 +
return true;
 
}
 
}
this.data=splitted.join('');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killChunks = function() { // heuristics alert
 
// chunks of italic text? you crazy, man?
 
var italicChunkRegex=new RegExp
 
("((^|\\n)\\s*:*\\s*''[^']([^']|'''|'[^']){20}(.|\\n[^\\n])*''[.!?\\s]*\\n)+", 'g');
 
// keep stuff separated, though, so stick in \n (fixes [[Union Jack]]?
 
this.data=this.data.replace(italicChunkRegex, '\n');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.mopup = function () {
 
// we simply *can't* be doing with horizontal rules right now
 
this.data=this.data.replace(RegExp('^-{4,}','mg'),'');
 
  
// no indented lines
+
function checkPopupPosition() {
this.data=this.data.replace(RegExp('(^|\\n) *:[^\\n]*','g'), '');
+
// stop the popup running off the right of the screen
 +
// FIXME avoid pg.current.link
 +
if (pg.current.link && pg.current.link.navpopup)
 +
pg.current.link.navpopup.limitHorizontalPosition();
 +
}
  
// replace __TOC__, __NOTOC__ and whatever else there is
+
function mouseOutWikiLink() {
// this'll probably do
+
//console ('mouseOutWikiLink');
this.data=this.data.replace(RegExp('^__[A-Z_]*__ *$', 'gmi'),'');
+
var a = this;
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.firstBit = function () {
 
// dont't be givin' me no subsequent paragraphs, you hear me?
 
/// first we "normalize" section headings, removing whitespace after, adding before
 
var d=this.data;
 
  
if (getValueOf('popupPreviewCutHeadings')) {
+
removeModifierKeyHandler(a);
this.data=this.data.replace(RegExp('\\s*(==+[^=]*==+)\\s*', 'g'), '\n\n$1 ');
 
/// then we want to get rid of paragraph breaks whose text ends badly
 
this.data=this.data.replace(RegExp('([:;]) *\\n{2,}', 'g'), '$1\n');
 
  
this.data=this.data.replace(RegExp('^[\\s\\n]*'), '');
+
if (a.navpopup === null || typeof a.navpopup === 'undefined') return;
stuff=(RegExp('^([^\\n]|\\n[^\\n\\s])*')).exec(this.data);
+
if (!a.navpopup.isVisible()) {
if (stuff) { d = stuff[0]; }
+
a.navpopup.banish();
if (!getValueOf('popupPreviewFirstParOnly')) { d = this.data; }
+
return;
 
+
}
/// now put \n\n after sections so that bullets and numbered lists work
+
restoreTitle(a);
d=d.replace(RegExp('(==+[^=]*==+)\\s*', 'g'), '$1\n\n');
+
Navpopup.tracker.addHook(posCheckerHook(a.navpopup));
 
}
 
}
  
 +
function posCheckerHook(navpop) {
 +
return function () {
 +
if (!navpop.isVisible()) {
 +
return true; /* remove this hook */
 +
}
 +
if (Navpopup.tracker.dirty) {
 +
return false;
 +
}
 +
var x = Navpopup.tracker.x,
 +
y = Navpopup.tracker.y;
 +
var mouseOverNavpop =
 +
navpop.isWithin(x, y, navpop.fuzz, navpop.mainDiv) ||
 +
!fuzzyCursorOffMenus(x, y, navpop.fuzz, navpop.mainDiv);
  
// Split sentences. Superfluous sentences are RIGHT OUT.
+
// FIXME it'd be prettier to do this internal to the Navpopup objects
// note: exactly 1 set of parens here needed to make the slice work
+
var t = getValueOf('popupHideDelay');
d = d.parenSplit(RegExp('([!?.]+["'+"'"+']*\\s)','g'));
+
if (t) {
// leading space is bad, mmkay?
+
t = t * 1000;
d[0]=d[0].replace(RegExp('^\\s*'), '');
+
}
 
+
if (!t) {
var notSentenceEnds=RegExp('([^.][a-z][.] *[a-z]|etc|sic|Dr|Mr|Mrs|Ms|St|no|op|cit|\\[[^\\]]*|\\s[A-Zvclm])$', 'i');
+
if (!mouseOverNavpop) {
d = this.fixSentenceEnds(d, notSentenceEnds);
+
if (navpop.parentAnchor) {
 
+
restoreTitle(navpop.parentAnchor);
this.fullLength=d.join('').length;
+
}
var maxChars=getValueOf('popupMaxPreviewCharacters') + this.extraCharacters;
+
navpop.banish();
var n=this.maxSentences;
+
return true; /* remove this hook */
var dd=this.firstSentences(d,n);  
+
}
 
+
return false;
do {
+
}
dd=this.firstSentences(d,n); --n;
+
// we have a hide delay set
} while ( dd.length > this.maxCharacters && n !== 0 );
+
var d = +new Date();
 
+
if (!navpop.mouseLeavingTime) {
this.data = dd;
+
navpop.mouseLeavingTime = d;
};
+
return false;
/**
+
}
  @private
+
if (mouseOverNavpop) {
*/
+
navpop.mouseLeavingTime = null;
Previewmaker.prototype.fixSentenceEnds = function(strs, reg) {
+
return false;
// take an array of strings, strs
+
}
// join strs[i] to strs[i+1] & strs[i+2] if strs[i] matches regex reg
+
if (d - navpop.mouseLeavingTime > t) {
 
+
navpop.mouseLeavingTime = null;
var abbrevRe=/\b[a-z][^a-z]*$/i;
+
navpop.banish();
 +
return true; /* remove this hook */
 +
}
 +
return false;
 +
};
 +
}
  
for (var i=0; i<strs.length-2; ++i) {
+
function runStopPopupTimer(navpop) {
if (reg.test(strs[i])) {
+
// at this point, we should have left the link but remain within the popup
var a=[];
+
// so we call this function again until we leave the popup.
for (var j=0; j<strs.length; ++j) {
+
if (!navpop.stopPopupTimer) {
if (j<i)  a[j]=strs[j];
+
navpop.stopPopupTimer = setInterval(posCheckerHook(navpop), 500);
if (j==i)  a[i]=strs[i]+strs[i+1]+strs[i+2];
+
navpop.addHook(
if (j>i+2) a[j-2]=strs[j];
+
function () {
}
+
clearInterval(navpop.stopPopupTimer);
return this.fixSentenceEnds(a,reg);
+
},
}
+
'hide',
// BUGGY STUFF - trying to fix up [[S. C. Johnson & Son]] preview
+
'before'
if (false && abbrevRe.test(strs[i])) {
+
);
var prevI=i, buf='';
 
do {
 
buf=buf+strs[i]+strs[i+1];
 
i=i+2;
 
} while (i<strs.length-2 && abbrevRe.test(strs[i]));
 
strs[i]=buf+strs[i];
 
return this.fixSentenceEnds((prevI?strs.slice(0,prevI-1):[]).concat(strs.slice(i)),
 
reg);
 
 
}
 
}
 
}
 
}
return strs;
+
// ENDFILE: mouseout.js
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.firstSentences = function(strs, howmany) {
 
var t=strs.slice(0, 2*howmany);
 
return t.join('');
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.killBadWhitespace = function() {
 
// also cleans up isolated '''', eg [[Suntory Sungoliath]]
 
this.data=this.data.replace(RegExp('^ *\'+ *$', 'gm'), '');
 
};
 
/**
 
  Runs the various methods to generate the preview.
 
  The preview is stored in the <code>html</html> field.
 
  @private
 
*/
 
Previewmaker.prototype.makePreview = function() {
 
if (this.owner.article.namespaceId()!=pg.nsTemplateId &&
 
this.owner.article.namespaceId()!=pg.nsImageId ) {
 
this.killComments();
 
this.killDivs();
 
this.killGalleries();
 
this.killBoxTemplates();
 
  
if (getValueOf('popupPreviewKillTemplates')) {
+
// STARTFILE: previewmaker.js
this.killTemplates();
+
/**
} else {
+
* @fileoverview
this.killMultilineTemplates();
+
* Defines the {@link Previewmaker} object, which generates short previews from wiki markup.
}
+
*/
this.killTables();
 
this.killImages();
 
this.killHTML();
 
this.killChunks();
 
this.mopup();
 
  
this.firstBit();
+
/**
this.killBadWhitespace();
+
* Creates a new Previewmaker
}
+
* @constructor
else
+
* @class The Previewmaker class. Use an instance of this to generate short previews from Wikitext.
{
+
* @param {String} wikiText The Wikitext source of the page we wish to preview.
this.killHTML();
+
* @param {String} baseUrl The url we should prepend when creating relative urls.
}
+
* @param {Navpopup} owner The navpop associated to this preview generator
this.html=wiki2html(this.data, this.baseUrl); // needs livepreview
+
*/
this.fixHTML();
+
function Previewmaker(wikiText, baseUrl, owner) {
this.stripLongTemplates();
+
/** The wikitext which is manipulated to generate the preview. */
};
+
this.originalData = wikiText;
/**
+
this.baseUrl = baseUrl;
  @private
+
this.owner = owner;
*/
 
Previewmaker.prototype.esWiki2HtmlPart = function(data) {
 
  var reLinks = /(?:\[\[([^|\]]*)(?:\|([^|\]]*))*]]([a-z]*))/gi; //match a wikilink
 
  reLinks.lastIndex = 0; //reset regex
 
  
  var match;
+
this.maxCharacters = getValueOf('popupMaxPreviewCharacters');
  var result = "";
+
this.maxSentences = getValueOf('popupMaxPreviewSentences');
  var postfixIndex = 0;
 
  while ((match = reLinks.exec(data))) //match all wikilinks
 
  {
 
//FIXME: the way that link is built here isn't perfect. It is clickable, but popups preview won't recognize it in some cases.
 
result += pg.escapeQuotesHTML(data.substring(postfixIndex, match.index)) +
 
  '<a href="'+Insta.conf.paths.articles+pg.escapeQuotesHTML(match[1])+'">'+pg.escapeQuotesHTML((match[2]?match[2]:match[1])+match[3])+"</a>";
 
postfixIndex = reLinks.lastIndex;
 
  }
 
  //append the rest
 
  result += pg.escapeQuotesHTML(data.substring(postfixIndex));
 
 
 
  return result;
 
};
 
Previewmaker.prototype.editSummaryPreview=function() {
 
var reAes  = /\/\* *(.*?) *\*\//g; //match the first section marker
 
reAes.lastIndex = 0; //reset regex
 
 
var match;
 
 
match = reAes.exec(this.data);
 
if (match)
 
{
 
//we have a section link. Split it, process it, combine it.
 
var prefix = this.data.substring(0,match.index-1);
 
var section = match[1];
 
var postfix = this.data.substring(reAes.lastIndex);
 
 
var start = "<span class='autocomment'>";
 
var end = "</span>";
 
if (prefix.length>0) start = this.esWiki2HtmlPart(prefix) + " " + start + "- ";
 
if (postfix.length>0) end = ": " + end + this.esWiki2HtmlPart(postfix);
 
 
  
var t=new Title().fromURL(this.baseUrl);
+
this.setData();
t.anchorFromUtf(section);
 
var sectionLink = Insta.conf.paths.articles + pg.escapeQuotesHTML(t.toString(true)) + '#' + pg.escapeQuotesHTML(t.anchor);
 
return start + '<a href="'+sectionLink+'">&rarr;</a> '+pg.escapeQuotesHTML(section) + end;
 
 
}
 
}
 
//else there's no section link, htmlify the whole thing.
 
return this.esWiki2HtmlPart(this.data);
 
};
 
  
//<NOLITE>
+
Previewmaker.prototype.setData = function () {
/** Test function for debugging preview problems one step at a time.
+
var maxSize = Math.max(10000, 2 * this.maxCharacters);
*/
+
this.data = this.originalData.substring(0, maxSize);
function previewSteps(txt) {
+
};
try {
 
txt=txt || document.editform.wpTextbox1.value;
 
} catch (err) {
 
if (pg.cache.pages.length > 0) {
 
txt=pg.cache.pages[pg.cache.pages.length-1].data;
 
} else {
 
alert('provide text or use an edit page');
 
}
 
}
 
txt=txt.substring(0,10000);
 
var base=pg.wiki.articlebase + Title.fromURL(document.location.href).urlString();
 
var p=new Previewmaker(txt, base, pg.current.link.navpopup);
 
if (this.owner.article.namespaceId() != pg.nsTemplateId) {
 
p.killComments(); if (!confirm('done killComments(). Continue?\n---\n' + p.data)) { return; }
 
p.killDivs(); if (!confirm('done killDivs(). Continue?\n---\n' + p.data)) { return; }
 
p.killGalleries(); if (!confirm('done killGalleries(). Continue?\n---\n' + p.data)) { return; }
 
p.killBoxTemplates(); if (!confirm('done killBoxTemplates(). Continue?\n---\n' + p.data)) { return; }
 
  
if (getValueOf('popupPreviewKillTemplates')) {
+
/**
p.killTemplates(); if (!confirm('done killTemplates(). Continue?\n---\n' + p.data)) { return; }
+
* Remove HTML comments
} else {
+
* @private
p.killMultilineTemplates(); if (!confirm('done killMultilineTemplates(). Continue?\n---\n' + p.data)) { return; }
+
*/
}
+
Previewmaker.prototype.killComments = function () {
 +
// this also kills one trailing newline, eg [[diamyo]]
 +
this.data = this.data.replace(
 +
RegExp('^<!--[^$]*?-->\\n|\\n<!--[^$]*?-->(?=\\n)|<!--[^$]*?-->', 'g'),
 +
''
 +
);
 +
};
  
p.killTables(); if (!confirm('done killTables(). Continue?\n---\n' + p.data)) { return; }
+
/**
p.killImages(); if (!confirm('done killImages(). Continue?\n---\n' + p.data)) { return; }
+
* @private
p.killHTML(); if (!confirm('done killHTML(). Continue?\n---\n' + p.data)) { return; }
+
*/
p.killChunks(); if (!confirm('done killChunks(). Continue?\n---\n' + p.data)) { return; }
+
Previewmaker.prototype.killDivs = function () {
p.mopup(); if (!confirm('done mopup(). Continue?\n---\n' + p.data)) { return; }
+
// say goodbye, divs (can be nested, so use * not *?)
 +
this.data = this.data.replace(RegExp('< *div[^>]* *>[\\s\\S]*?< */ *div *>', 'gi'), '');
 +
};
  
p.firstBit(); if (!confirm('done firstBit(). Continue?\n---\n' + p.data)) { return; }
+
/**
p.killBadWhitespace(); if (!confirm('done killBadWhitespace(). Continue?\n---\n' + p.data)) { return; }
+
* @private
}
+
*/
 
+
Previewmaker.prototype.killGalleries = function () {
p.html=wiki2html(p.data, base); // needs livepreview
+
this.data = this.data.replace(RegExp('< *gallery[^>]* *>[\\s\\S]*?< */ *gallery *>', 'gi'), '');
p.fixHTML(); if (!confirm('done fixHTML(). Continue?\n---\n' + p.html)) { return; }
 
p.stripLongTemplates(); if (!confirm('done stripLongTemplates(). Continue?\n---\n' + p.html)) { return; }
 
alert('finished preview - end result follows.\n---\n' + p.html);
 
}
 
//</NOLITE>
 
 
 
/**
 
  Works around livepreview bugs.
 
  @private
 
*/
 
Previewmaker.prototype.fixHTML = function() {
 
if(!this.html) return;
 
 
 
  var ret = this.html;
 
 
 
// fix question marks in wiki links
 
// maybe this'll break some stuff :-(
 
ret=ret.replace(RegExp('\(<a href="' + pg.wiki.articlePath + '/[^"]*\)[?]\(.*?"\)', 'g'), '$1%3F$2');
 
ret=ret.replace(RegExp('\(<a href=\'' + pg.wiki.articlePath + '/[^\']*\)[?]\(.*?\'\)', 'g'), '$1%3F$2');
 
// FIXME fix up % too
 
 
 
this.html=ret;
 
};
 
/**
 
  Generates the preview and displays it in the current popup.
 
 
 
  Does nothing if the generated preview is invalid or consists of whitespace only.
 
  Also activates wikilinks in the preview for subpopups if the popupSubpopups option is true.
 
*/
 
Previewmaker.prototype.showPreview = function () {
 
this.makePreview();
 
if (typeof this.html != typeof '') return;
 
if (RegExp('^\\s*$').test(this.html)) return;
 
setPopupHTML('<hr />', 'popupPrePreviewSep', this.owner.idNumber);
 
setPopupTipsAndHTML(this.html, 'popupPreview', this.owner.idNumber, { owner: this.owner });
 
var more = (this.fullLength > this.data.length) ? this.moreLink() : '';
 
setPopupHTML(more, 'popupPreviewMore', this.owner.idNumber);
 
};
 
/**
 
  @private
 
*/
 
Previewmaker.prototype.moreLink=function() {
 
var a=document.createElement('a');
 
a.className='popupMoreLink';
 
a.innerHTML=popupString('more...');
 
var savedThis=this;
 
a.onclick=function() {
 
savedThis.maxCharacters+=2000;
 
savedThis.maxSentences+=20;
 
savedThis.setData();
 
savedThis.showPreview();
 
 
};
 
};
return a;
 
};
 
  
/**
+
/**
  @private
+
* @private
*/
+
*/
Previewmaker.prototype.stripLongTemplates = function() {
+
Previewmaker.prototype.kill = function (opening, closing, subopening, subclosing, repl) {
// operates on the HTML!
+
var oldk = this.data;
this.html=this.html.replace(RegExp('^.{0,1000}[{][{][^}]*?(<(p|br)( /)?>\\s*){2,}([^{}]*?[}][}])?', 'gi'), '');
+
var k = this.killStuff(this.data, opening, closing, subopening, subclosing, repl);
this.html=this.html.split('\n').join(' '); // workaround for <pre> templates
+
while (k.length < oldk.length) {
this.html=this.html.replace(RegExp('[{][{][^}]*<pre>[^}]*[}][}]','gi'), '');
+
oldk = k;
};
+
k = this.killStuff(k, opening, closing, subopening, subclosing, repl);
/**
 
  @private
 
*/
 
Previewmaker.prototype.killMultilineTemplates = function() {
 
this.kill('{{{', '}}}');
 
this.kill(RegExp('\\s*[{][{][^{}]*\\n'), '}}', '{{');
 
};
 
// ENDFILE: previewmaker.js
 
// STARTFILE: querypreview.js
 
function loadAPIPreview(queryType, article, navpop) {
 
var art=new Title(article).urlString();
 
var url=pg.wiki.apiwikibase + '?format=json&action=query&';
 
var htmlGenerator=function(a,d){alert('invalid html generator');};
 
var usernameart = '';
 
switch (queryType) {
 
case 'history':
 
url += 'meta=userinfo&uiprop=options&titles=' + art + '&prop=revisions&rvlimit=' +
 
getValueOf('popupHistoryPreviewLimit');
 
htmlGenerator=APIhistoryPreviewHTML;
 
break;
 
case 'category':
 
url += 'list=categorymembers&rawcontinue=&cmtitle=' + art;
 
htmlGenerator=APIcategoryPreviewHTML;
 
break;
 
case 'userinfo':
 
var username = new Title( article ).userName();
 
usernameart = encodeURIComponent( username );
 
if (pg.re.ipUser.test(username)) {
 
url += 'list=blocks&bkprop=range&bkip=' + usernameart;
 
} else {
 
url += 'list=users|usercontribs&usprop=blockinfo|groups|editcount|registration|gender&ususers=' + usernameart + "&meta=globaluserinfo&guiprop=groups|unattached&guiuser="+ usernameart + "&uclimit=1&ucprop=timestamp&ucuser=" + usernameart;
 
 
}
 
}
htmlGenerator=APIuserInfoPreviewHTML;
+
this.data = k;
break;
 
case 'contribs':
 
usernameart = encodeURIComponent( new Title( article ).userName() );
 
url += 'list=usercontribs&meta=userinfo&uiprop=options&ucuser=' + usernameart +
 
'&uclimit=' + getValueOf('popupContribsPreviewLimit');
 
htmlGenerator=APIcontribsPreviewHTML;
 
break;
 
case 'imagepagepreview':
 
var trail='';
 
if (getValueOf('popupImageLinks')) { trail = '&list=imageusage&iutitle=' + art; }
 
url += 'titles=' + art + '&prop=revisions|imageinfo&rvprop=content' + trail;
 
htmlGenerator=APIimagepagePreviewHTML;
 
break;
 
case 'backlinks':
 
url += 'list=backlinks&rawcontinue=&bltitle=' + art;
 
htmlGenerator=APIbacklinksPreviewHTML;
 
break;
 
}
 
pendingNavpopTask(navpop);
 
if( !mw.config.get('wgEnableAPI') ) {
 
/* The API is not available */
 
htmlGenerator=function(a,d){
 
return 'This function of navigation popups now requires a MediaWiki ' +
 
'installation with the <a href="http://www.mediawiki.org/wiki/API">API</a> enabled.'; };
 
}
 
var callback=function(d){
 
log( "callback of API functions was hit" );
 
showAPIPreview(queryType, htmlGenerator(article,d,navpop), navpop.idNumber, navpop, d);
 
};
 
if (pg.flag.isIE) {
 
url = url + '&*'; //to circumvent https://bugzilla.wikimedia.org/show_bug.cgi?id=28840
 
}
 
var go = function(){
 
getPageWithCaching(url, callback, navpop);
 
return true;
 
 
};
 
};
if (navpop.visible || !getValueOf('popupLazyDownloads')) { go(); }
 
else { navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_'+queryType+'_QUERY_DATA'); }
 
}
 
  
function linkList(list) {
+
/**
list.sort(function(x,y) { return (x==y ? 0 : (x<y ? -1 : 1)); });
+
* @private
var buf=[];
+
*/
for (var i=0; i<list.length; ++i) {
+
Previewmaker.prototype.killStuff = function (
buf.push(wikiLink({article: new Title(list[i]),
+
txt,
  text: list[i].split(' ').join('&nbsp;'),
+
opening,
  action: 'view'}));
+
closing,
}
+
subopening,
return buf.join(', ');
+
subclosing,
}
+
repl
 
+
) {
function getTimeOffset(tz) {
+
var op = this.makeRegexp(opening);
if( tz ) {
+
var cl = this.makeRegexp(closing, '^');
if( tz.indexOf('|') > -1 ) {
+
var sb = subopening ? this.makeRegexp(subopening, '^') : null;
// New format
+
var sc = subclosing ? this.makeRegexp(subclosing, '^') : cl;
return parseInt(tz.split('|')[1],10);
+
if (!op || !cl) {
} else if ( tz.indexOf(':') > -1 ) {
+
alert('Navigation Popups error: op or cl is null! something is wrong.');
// Old format
+
return;
return( parseInt(tz,10)*60 + parseInt(tz.split(':')[1],10) );
 
 
}
 
}
}
+
if (!op.test(txt)) {
return 0;
+
return txt;
}
 
 
 
function editPreviewTable(article, h, reallyContribs, timeOffset) {
 
var html=['<table>'];
 
var day=null;
 
var curart=article;
 
var page=null;
 
for (var i=0; i<h.length; ++i) {
 
if (reallyContribs) {  
 
page=h[i].title; curart = new Title(page);
 
 
}
 
}
var minor=typeof h[i].minor=='undefined' ? '' : '<b>m </b>';
+
var ret = '';
var editDate=adjustDate(getDateFromTimestamp(h[i].timestamp), timeOffset);
+
var opResult = op.exec(txt);
var thisDay = dayFormat(editDate);
+
ret = txt.substring(0, opResult.index);
var thisTime = timeFormat(editDate);
+
txt = txt.substring(opResult.index + opResult[0].length);
if (thisDay==day) { thisDay=''; }
+
var depth = 1;
else { day=thisDay; }
+
while (txt.length > 0) {
if (thisDay) {
+
var removal = 0;
html.push( '<tr><td colspan=3><span class="popup_history_date">' +
+
if (depth == 1 && cl.test(txt)) {
  thisDay+'</span></td></tr>' );
+
depth--;
 +
removal = cl.exec(txt)[0].length;
 +
} else if (depth > 1 && sc.test(txt)) {
 +
depth--;
 +
removal = sc.exec(txt)[0].length;
 +
} else if (sb && sb.test(txt)) {
 +
depth++;
 +
removal = sb.exec(txt)[0].length;
 +
}
 +
if (!removal) {
 +
removal = 1;
 +
}
 +
txt = txt.substring(removal);
 +
if (depth === 0) {
 +
break;
 +
}
 
}
 
}
html.push('<tr class="popup_history_row_' + ( (i%2) ? 'odd' : 'even') + '">');
+
return ret + (repl || '') + txt;
html.push('<td>(<a href="' + pg.wiki.titlebase + new Title(curart).urlString() +
+
};
'&diff=prev&oldid=' + h[i].revid + '">' + popupString('last') + '</a>)</td>');
+
 
html.push('<td>' +
+
/**
'<a href="' + pg.wiki.titlebase + new Title(curart).urlString() +
+
* @private
'&oldid=' + h[i].revid + '">' + thisTime + '</a></td>');
+
*/
var col3url='', col3txt='';
+
Previewmaker.prototype.makeRegexp = function (x, prefix, suffix) {
if (!reallyContribs) {
+
prefix = prefix || '';
var user=h[i].user;
+
suffix = suffix || '';
if( typeof h[i].userhidden == "undefined" ) {
+
var reStr = '';
if( pg.re.ipUser.test(user) ) {
+
var flags = '';
col3url=pg.wiki.titlebase + mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] + ':Contributions&target=' + new Title(user).urlString();
+
if (isString(x)) {
} else {
+
reStr = prefix + literalizeRegex(x) + suffix;
col3url=pg.wiki.titlebase + mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + new Title(user).urlString();
+
} else if (isRegExp(x)) {
}
+
var s = x.toString().substring(1);
col3txt=pg.escapeQuotesHTML(user);
+
var sp = s.split('/');
} else {
+
flags = sp[sp.length - 1];
col3url=getValueOf('popupRevDelUrl');
+
sp[sp.length - 1] = '';
col3txt=pg.escapeQuotesHTML( popupString('revdel'));
+
s = sp.join('/');
}
+
s = s.substring(0, s.length - 1);
 +
reStr = prefix + s + suffix;
 
} else {
 
} else {
col3url=pg.wiki.titlebase + curart.urlString();
+
log('makeRegexp failed');
col3txt=pg.escapeQuotesHTML(page);
 
 
}
 
}
html.push('<td>' + (reallyContribs ? minor : '') +
 
'<a href="' + col3url + '">' + col3txt + '</a></td>');
 
var comment='';
 
var c=h[i].comment || h[i]['*'];
 
if (c) {
 
comment=new Previewmaker(c, new Title(curart).toUrl()).editSummaryPreview();
 
} else if (typeof h[i].commenthidden != "undefined" ) {
 
comment=popupString('revdel');
 
}
 
html.push('<td>' + (!reallyContribs ? minor : '') + comment + '</td>');
 
html.push('</tr>');
 
html=[html.join('')];
 
}
 
html.push('</table>');
 
return html.join('');
 
}
 
  
function getDateFromTimestamp(t) {
+
log('makeRegexp: got reStr=' + reStr + ', flags=' + flags);
var s=t.split(/[^0-9]/);
+
return RegExp(reStr, flags);
switch(s.length) {
+
};
case 0: return null;
+
 
case 1: return new Date(s[0]);
+
/**
case 2: return new Date(s[0], s[1]-1);
+
* @private
case 3: return new Date(s[0], s[1]-1, s[2]);
+
*/
case 4: return new Date(s[0], s[1]-1, s[2], s[3]);
+
Previewmaker.prototype.killBoxTemplates = function () {
case 5: return new Date(s[0], s[1]-1, s[2], s[3], s[4]);
+
// taxobox removal... in fact, there's a saudiprincebox_begin, so let's be more general
case 6: return new Date(s[0], s[1]-1, s[2], s[3], s[4], s[5]);
+
// also, have float_begin, ... float_end
default: return new Date(s[0], s[1]-1, s[2], s[3], s[4], s[5], s[6]);
+
this.kill(RegExp('[{][{][^{}\\s|]*?(float|box)[_ ](begin|start)', 'i'), /[}][}]\s*/, '{{');
}
 
}
 
  
function adjustDate(d, offset) {
+
// infoboxes etc
// offset is in minutes
+
// from [[User:Zyxw/popups.js]]: kill frames too
var o=offset * 60 * 1000;
+
this.kill(RegExp('[{][{][^{}\\s|]*?(infobox|elementbox|frame)[_ ]', 'i'), /[}][}]\s*/, '{{');
return new Date( +d + o);
+
};
}
 
  
function dayFormat(editDate, utc) {
+
/**
if (utc) { return map(zeroFill, [editDate.getUTCFullYear(), editDate.getUTCMonth()+1, editDate.getUTCDate()]).join('-'); }
+
* @private
return map(zeroFill, [editDate.getFullYear(), editDate.getMonth()+1, editDate.getDate()]).join('-');
+
*/
}
+
Previewmaker.prototype.killTemplates = function () {
 +
this.kill('{{', '}}', '{', '}', ' ');
 +
};
  
function timeFormat(editDate, utc) {
+
/**
if (utc) { return map(zeroFill, [editDate.getUTCHours(), editDate.getUTCMinutes(), editDate.getUTCSeconds()]).join(':'); }
+
* @private
return map(zeroFill, [editDate.getHours(), editDate.getMinutes(), editDate.getSeconds()]).join(':');
+
*/
}
+
Previewmaker.prototype.killTables = function () {
 +
// tables are bad, too
 +
// this can be slow, but it's an inprovement over a browser hang
 +
// torture test: [[Comparison_of_Intel_Central_Processing_Units]]
 +
this.kill('{|', /[|]}\s*/, '{|');
 +
this.kill(/<table.*?>/i, /<\/table.*?>/i, /<table.*?>/i);
 +
// remove lines starting with a pipe for the hell of it (?)
 +
this.data = this.data.replace(RegExp('^[|].*$', 'mg'), '');
 +
};
  
function showAPIPreview(queryType, html, id, navpop, download) {
+
/**
// DJ: done
+
* @private
var target='popupPreview';
+
*/
switch (queryType) {
+
Previewmaker.prototype.killImages = function () {
case 'imagelinks':
+
var forbiddenNamespaceAliases = [];
case 'category':
+
jQuery.each(mw.config.get('wgNamespaceIds'), function (_localizedNamespaceLc, _namespaceId) {
case 'userinfo':
+
if (_namespaceId != pg.nsImageId && _namespaceId != pg.nsCategoryId) return;
target='popupPostPreview'; break;
+
forbiddenNamespaceAliases.push(_localizedNamespaceLc.split(' ').join('[ _]')); //todo: escape regexp fragments!
}
+
});
setPopupTipsAndHTML(html, target, id);
 
completedNavpopTask(navpop);
 
}
 
  
function APIbacklinksPreviewHTML(article, download, navpop) {
+
// images and categories are a nono
try {
+
this.kill(
var jsObj=getJsObj(download.data);
+
RegExp('[[][[]\\s*(' + forbiddenNamespaceAliases.join('|') + ')\\s*:', 'i'),
var list=jsObj.query.backlinks;
+
/\]\]\s*/,
 +
'[',
 +
']'
 +
);
 +
};
  
var html=[];
+
/**
if (!list) { return popupString('No backlinks found'); }
+
* @private
for ( var i=0; i < list.length; i++ ) {
+
*/
var t=new Title(list[i].title);
+
Previewmaker.prototype.killHTML = function () {
html.push('<a href="' + pg.wiki.titlebase + t.urlString() + '">' + t + '</a>');
+
// kill <ref ...>...</ref>
}
+
this.kill(/<ref\b[^/>]*?>/i, /<\/ref>/i);
html=html.join(', ');
 
if (jsObj['query-continue'] && jsObj['query-continue'].backlinks && jsObj['query-continue'].backlinks.blcontinue) {
 
html += popupString(' and more');
 
}
 
return html;
 
} catch (someError) {
 
return 'backlinksPreviewHTML went wonky';
 
}
 
}
 
  
pg.fn.APIsharedImagePagePreviewHTML = function APIsharedImagePagePreviewHTML(obj) {
+
// let's also delete entire lines starting with <. it's worth a try.
log( "APIsharedImagePagePreviewHTML" );
+
this.data = this.data.replace(RegExp('(^|\\n) *<.*', 'g'), '\n');
var popupid = obj.requestid;
 
if( obj.query && obj.query.pages )
 
{
 
var page=anyChild(obj.query.pages );
 
var content=(page && page.revisions ) ? page.revisions[0]['*'] : null;
 
if( content )  
 
{
 
/* Not entirely safe, but the best we can do */
 
var p=new Previewmaker(content, pg.current.link.navpopup.article, pg.current.link.navpopup);
 
p.makePreview();
 
setPopupHTML( p.html, "popupSecondPreview", popupid );
 
}
 
}
 
};
 
  
function APIimagepagePreviewHTML(article, download, navpop) {
+
// and those pesky html tags, but not <nowiki> or <blockquote>
try {
+
var splitted = this.data.parenSplit(/(<[\w\W]*?(?:>|$|(?=<)))/);
var jsObj=getJsObj(download.data);
+
var len = splitted.length;
var page=anyChild(jsObj.query.pages);
+
for (var i = 1; i < len; i = i + 2) {
var content=(page && page.revisions ) ? page.revisions[0]['*'] : null;
+
switch (splitted[i]) {
var ret='';
+
case '<nowiki>':
var alt='';
+
case '</nowiki>':
try{alt=navpop.parentAnchor.childNodes[0].alt;} catch(e){}
+
case '<blockquote>':
if (alt) {
+
case '</blockquote>':
ret = ret + '<hr /><b>' + popupString('Alt text:') + '</b> ' + pg.escapeQuotesHTML(alt);
+
break;
}
+
default:
if (content) {
+
splitted[i] = '';
var p=prepPreviewmaker(content, article, navpop);
 
p.makePreview();
 
if (p.html) { ret += '<hr />' + p.html; }
 
if (getValueOf('popupSummaryData')) {
 
var info=getPageInfo(content, download);
 
log(info);
 
setPopupTrailer(info, navpop.idNumber);
 
 
}
 
}
 
}
 
}
if (page && page.imagerepository == "shared" ) {
+
this.data = splitted.join('');
var art=new Title(article);
+
};
var encart = encodeURIComponent( "File:" + art.stripNamespace() );
+
 
var shared_url =  pg.wiki.apicommonsbase + '?format=json&callback=pg.fn.APIsharedImagePagePreviewHTML' +
+
/**
'&requestid=' + navpop.idNumber +
+
* @private
'&action=query&prop=revisions&rvprop=content&titles=' + encart;
+
*/
if (pg.flag.isIE) {
+
Previewmaker.prototype.killChunks = function () {
shared_url = shared_url + '&*'; //to circumvent https://bugzilla.wikimedia.org/show_bug.cgi?id=28840
+
// heuristics alert
}
+
// chunks of italic text? you crazy, man?
 +
var italicChunkRegex = new RegExp(
 +
"((^|\\n)\\s*:*\\s*''[^']([^']|'''|'[^']){20}(.|\\n[^\\n])*''[.!?\\s]*\\n)+",
 +
'g'
 +
);
 +
// keep stuff separated, though, so stick in \n (fixes [[Union Jack]]?
 +
this.data = this.data.replace(italicChunkRegex, '\n');
 +
};
 +
 
 +
/**
 +
* @private
 +
*/
 +
Previewmaker.prototype.mopup = function () {
 +
// we simply *can't* be doing with horizontal rules right now
 +
this.data = this.data.replace(RegExp('^-{4,}', 'mg'), '');
 +
 
 +
// no indented lines
 +
this.data = this.data.replace(RegExp('(^|\\n) *:[^\\n]*', 'g'), '');
  
ret = ret +'<hr />' + popupString( 'Image from Commons') +
+
// replace __TOC__, __NOTOC__ and whatever else there is
': <a href="' + pg.wiki.commonsbase + '?title=' + encart + '">' +
+
// this'll probably do
popupString( 'Description page') + '</a>';
+
this.data = this.data.replace(RegExp('^__[A-Z_]*__ *$', 'gmi'), '');
mw.loader.load( shared_url );
+
};
}
 
showAPIPreview('imagelinks', APIimagelinksPreviewHTML(article,download), navpop.idNumber, download);
 
return ret;
 
} catch (someError) {
 
return 'API imagepage preview failed :(';
 
}
 
}
 
  
function APIimagelinksPreviewHTML(article, download) {
+
/**
try {
+
* @private
var jsobj=getJsObj(download.data);
+
*/
var list=jsobj.query.imageusage;
+
Previewmaker.prototype.firstBit = function () {
if (list) {
+
// dont't be givin' me no subsequent paragraphs, you hear me?
var ret=[];
+
/// first we "normalize" section headings, removing whitespace after, adding before
for (var i=0; i < list.length; i++) {
+
var d = this.data;
ret.push(list[i].title);
 
}
 
if (ret.length === 0) { return popupString('No image links found'); }
 
return '<h2>' + popupString('File links') + '</h2>' + linkList(ret);
 
} else {
 
return popupString('No image links found');
 
}
 
} catch(someError) {
 
return 'Image links preview generation failed :(';
 
}
 
}
 
  
function APIcategoryPreviewHTML(article, download) {
+
if (getValueOf('popupPreviewCutHeadings')) {
try{
+
this.data = this.data.replace(RegExp('\\s*(==+[^=]*==+)\\s*', 'g'), '\n\n$1 ');
var jsobj=getJsObj(download.data);
+
/// then we want to get rid of paragraph breaks whose text ends badly
var list=jsobj.query.categorymembers;
+
this.data = this.data.replace(RegExp('([:;]) *\\n{2,}', 'g'), '$1\n');
var ret=[];
 
for (var p=0; p < list.length; p++) {
 
  ret.push(list[p].title);
 
}
 
if (ret.length === 0) { return popupString('Empty category'); }
 
ret = '<h2>' + tprintf('Category members (%s shown)', [ret.length]) + '</h2>' +linkList(ret);
 
if (jsobj['query-continue'] && jsobj['query-continue'].categorymembers && jsobj['query-continue'].categorymembers.cmcontinue) {
 
ret += popupString(' and more');
 
}
 
return ret;
 
} catch(someError) {
 
return 'Category preview failed :(';
 
}
 
}
 
  
function APIuserInfoPreviewHTML(article, download) {
+
this.data = this.data.replace(RegExp('^[\\s\\n]*'), '');
var ret=[];
+
var stuff = RegExp('^([^\\n]|\\n[^\\n\\s])*').exec(this.data);
var queryobj = {};
+
if (stuff) {
try{
+
d = stuff[0];
queryobj=getJsObj(download.data).query;
 
} catch(someError) { return 'Userinfo preview failed :('; }
 
 
var user=anyChild(queryobj.users);
 
if (user) {
 
var globaluserinfo=queryobj.globaluserinfo;
 
if (user.invalid === '') {
 
ret.push( popupString( 'Invalid user') );
 
} else if (user.missing === '') {
 
ret.push( popupString( 'Not a registered username') );
 
}
 
if( user.blockedby )
 
ret.push('<b>' + popupString('BLOCKED') + '</b>');
 
log(globaluserinfo);
 
if( globaluserinfo && ( globaluserinfo.locked || globaluserinfo.hidden ) ) {
 
var lockedSulAccountIsAttachedToThis = true;
 
for( var i=0; globaluserinfo.unattached && i < globaluserinfo.unattached.length; i++) {
 
if ( globaluserinfo.unattached[i].wiki === mw.config.get('wgDBname') ) {
 
lockedSulAccountIsAttachedToThis=false;
 
break;
 
}
 
 
}
 
}
if (lockedSulAccountIsAttachedToThis) {
+
if (!getValueOf('popupPreviewFirstParOnly')) {
if (globaluserinfo.locked) ret.push('<b><i>' + popupString('LOCKED') + '</i></b>');
+
d = this.data;
if (globaluserinfo.hidden) ret.push('<b><i>' + popupString('HIDDEN') + '</i></b>');
 
 
}
 
}
 +
 +
/// now put \n\n after sections so that bullets and numbered lists work
 +
d = d.replace(RegExp('(==+[^=]*==+)\\s*', 'g'), '$1\n\n');
 
}
 
}
if( getValueOf('popupShowGender') && user.gender ) {
+
 
switch( user.gender ) {
+
// Split sentences. Superfluous sentences are RIGHT OUT.
case "male": ret.push( popupString( "\u2642" ) ); break;
+
// note: exactly 1 set of parens here needed to make the slice work
case "female": ret.push( popupString( "\u2640" ) ); break;
+
d = d.parenSplit(RegExp('([!?.]+["' + "'" + ']*\\s)', 'g'));
}
+
// leading space is bad, mmkay?
}
+
d[0] = d[0].replace(RegExp('^\\s*'), '');
if( user.groups ) {
+
 
for( var j=0; j < user.groups.length; j++) {
+
var notSentenceEnds = RegExp(
var currentGroup = user.groups[j];
+
'([^.][a-z][.] *[a-z]|etc|sic|Dr|Mr|Mrs|Ms|St|no|op|cit|\\[[^\\]]*|\\s[A-Zvclm])$',
if( ["*", "user", "autoconfirmed", "extendedconfirmed"].indexOf( currentGroup ) === -1 ) {
+
'i'
ret.push( pg.escapeQuotesHTML(user.groups[j]) );
+
);
 +
d = this.fixSentenceEnds(d, notSentenceEnds);
 +
 
 +
this.fullLength = d.join('').length;
 +
var n = this.maxSentences;
 +
var dd = this.firstSentences(d, n);
 +
 
 +
do {
 +
dd = this.firstSentences(d, n);
 +
--n;
 +
} while (dd.length > this.maxCharacters && n !== 0);
 +
 
 +
this.data = dd;
 +
};
 +
 
 +
/**
 +
* @private
 +
*/
 +
Previewmaker.prototype.fixSentenceEnds = function (strs, reg) {
 +
// take an array of strings, strs
 +
// join strs[i] to strs[i+1] & strs[i+2] if strs[i] matches regex reg
 +
 
 +
for (var i = 0; i < strs.length - 2; ++i) {
 +
if (reg.test(strs[i])) {
 +
var a = [];
 +
for (var j = 0; j < strs.length; ++j) {
 +
if (j < i) a[j] = strs[j];
 +
if (j == i) a[i] = strs[i] + strs[i + 1] + strs[i + 2];
 +
if (j > i + 2) a[j - 2] = strs[j];
 
}
 
}
 +
return this.fixSentenceEnds(a, reg);
 
}
 
}
 
}
 
}
if( globaluserinfo && globaluserinfo.groups ) {
+
return strs;
for( var k=0; k < globaluserinfo.groups.length; k++) {
+
};
ret.push( '<i>'+pg.escapeQuotesHTML(globaluserinfo.groups[k])+'</i>' );
+
 
}
+
/**
}
+
* @private
if( user.editcount || user.registration )
+
*/
ret.push( pg.escapeQuotesHTML((user.editcount?user.editcount:'') + popupString(' edits since: ') + (user.registration?dayFormat(getDateFromTimestamp(user.registration)):'')) );
+
Previewmaker.prototype.firstSentences = function (strs, howmany) {
}
+
var t = strs.slice(0, 2 * howmany);
 +
return t.join('');
 +
};
 +
 
 +
/**
 +
* @private
 +
*/
 +
Previewmaker.prototype.killBadWhitespace = function () {
 +
// also cleans up isolated '''', eg [[Suntory Sungoliath]]
 +
this.data = this.data.replace(RegExp("^ *'+ *$", 'gm'), '');
 +
};
  
if (queryobj.usercontribs) {
+
/**
ret.push( popupString('last edit on ') + dayFormat(getDateFromTimestamp(queryobj.usercontribs[0].timestamp)) );
+
* Runs the various methods to generate the preview.
}
+
* The preview is stored in the <code>html</html> field.
+
* @private
if (queryobj.blocks) {
+
*/
ret.push( popupString( 'IP user') ); //we only request list=blocks for IPs
+
Previewmaker.prototype.makePreview = function () {
for (var l=0; l<queryobj.blocks.length; l++) {
+
if (
ret.push('<b>' + popupString(queryobj.blocks[l].rangestart === queryobj.blocks[l].rangeend ? 'BLOCKED' : 'RANGEBLOCKED') + '</b>' );
+
this.owner.article.namespaceId() != pg.nsTemplateId &&
}
+
this.owner.article.namespaceId() != pg.nsImageId
}
+
) {
+
this.killComments();
ret = '<hr />' + ret.join( ', ' );
+
this.killDivs();
return ret;
+
this.killGalleries();
}
+
this.killBoxTemplates();
  
function APIcontribsPreviewHTML(article, download, navpop) {
+
if (getValueOf('popupPreviewKillTemplates')) {
return APIhistoryPreviewHTML(article, download, navpop, true);
+
this.killTemplates();
}
+
} else {
 +
this.killMultilineTemplates();
 +
}
 +
this.killTables();
 +
this.killImages();
 +
this.killHTML();
 +
this.killChunks();
 +
this.mopup();
  
function APIhistoryPreviewHTML(article, download, navpop, reallyContribs) {
+
this.firstBit();
try {
+
this.killBadWhitespace();
var jsobj=getJsObj(download.data);
 
var tz=jsobj.query.userinfo.options.timecorrection;
 
var edits = [];
 
if( reallyContribs ) {
 
edits=jsobj.query.usercontribs;
 
 
} else {
 
} else {
edits=anyChild(jsobj.query.pages).revisions;
+
this.killHTML();
 
}
 
}
var timeOffset = getTimeOffset(tz);
+
this.html = wiki2html(this.data, this.baseUrl); // needs livepreview
Cookie.create('popTz', timeOffset, 1);
+
this.fixHTML();
 +
this.stripLongTemplates();
 +
};
  
var ret=editPreviewTable(article, edits, reallyContribs, timeOffset);
+
/**
return ret;
+
* @private
} catch (someError) {
+
*/
return 'History preview failed :-(';
+
Previewmaker.prototype.esWiki2HtmlPart = function (data) {
}
+
var reLinks = /(?:\[\[([^|\]]*)(?:\|([^|\]]*))*]]([a-z]*))/gi; //match a wikilink
}
+
reLinks.lastIndex = 0; //reset regex
  
 +
var match;
 +
var result = '';
 +
var postfixIndex = 0;
 +
while ((match = reLinks.exec(data))) {
 +
//match all wikilinks
 +
//FIXME: the way that link is built here isn't perfect. It is clickable, but popups preview won't recognize it in some cases.
 +
result +=
 +
pg.escapeQuotesHTML(data.substring(postfixIndex, match.index)) +
 +
'<a href="' +
 +
Insta.conf.paths.articles +
 +
pg.escapeQuotesHTML(match[1]) +
 +
'">' +
 +
pg.escapeQuotesHTML((match[2] ? match[2] : match[1]) + match[3]) +
 +
'</a>';
 +
postfixIndex = reLinks.lastIndex;
 +
}
 +
//append the rest
 +
result += pg.escapeQuotesHTML(data.substring(postfixIndex));
  
//</NOLITE>
+
return result;
// ENDFILE: querypreview.js
+
};
// STARTFILE: debug.js
+
Previewmaker.prototype.editSummaryPreview = function () {
////////////////////////////////////////////////////////////////////
+
var reAes = /\/\* *(.*?) *\*\//g; //match the first section marker
// Debugging functions
+
reAes.lastIndex = 0; //reset regex
////////////////////////////////////////////////////////////////////
 
  
function setupDebugging() {
+
var match;
//<NOLITE>
 
if (window.popupDebug) { // popupDebug is set from .version
 
window.log=function(x) { //if(gMsg!='')gMsg += '\n'; gMsg+=time() + ' ' + x; };
 
window.console.log(x);
 
};
 
window.errlog=function(x) {
 
window.console.error(x);
 
};
 
log('Initializing logger');
 
} else {
 
//</NOLITE>
 
window.log = function(x) {};
 
window.errlog = function(x) {};
 
//<NOLITE>
 
}
 
//</NOLITE>
 
}
 
// ENDFILE: debug.js
 
// STARTFILE: images.js
 
  
// load image of type Title.
+
match = reAes.exec(this.data);
function loadImage(image, navpop) {
+
if (match) {
if (typeof image.stripNamespace != 'function') { alert('loadImages bad'); }
+
//we have a section link. Split it, process it, combine it.
// API call to retrieve image info.
+
var prefix = this.data.substring(0, match.index - 1);
 +
var section = match[1];
 +
var postfix = this.data.substring(reAes.lastIndex);
  
if ( !getValueOf('popupImages') || !mw.config.get('wgEnableAPI') ) return;
+
var start = "<span class='autocomment'>";
if ( !isValidImageName(image) ) return false;
+
var end = '</span>';
+
if (prefix.length > 0) start = this.esWiki2HtmlPart(prefix) + ' ' + start + '- ';
var art=image.urlString();
+
if (postfix.length > 0) end = ': ' + end + this.esWiki2HtmlPart(postfix);
  
var url=pg.wiki.apiwikibase + '?format=json&action=query';
+
var t = new Title().fromURL(this.baseUrl);
url += '&prop=imageinfo&iiprop=url|mime&iiurlwidth=' + getValueOf('popupImageSizeLarge');
+
t.anchorFromUtf(section);
url += '&titles=' + art;
+
var sectionLink =
if (pg.flag.isIE) {
+
Insta.conf.paths.articles +
url = url + '&*'; //to circumvent https://bugzilla.wikimedia.org/show_bug.cgi?id=28840
+
pg.escapeQuotesHTML(t.toString(true)) +
}
+
'#' +
 +
pg.escapeQuotesHTML(t.anchor);
 +
return (
 +
start + '<a href="' + sectionLink + '">&rarr;</a> ' + pg.escapeQuotesHTML(section) + end
 +
);
 +
}
  
pendingNavpopTask(navpop);
+
//else there's no section link, htmlify the whole thing.
var callback=function(d){
+
return this.esWiki2HtmlPart(this.data);
popupsInsertImage(navpop.idNumber, navpop, d);
 
 
};
 
};
var go = function(){
 
getPageWithCaching(url, callback, navpop);
 
return true;
 
};
 
if (navpop.visible || !getValueOf('popupLazyDownloads')) { go(); }
 
else { navpop.addHook(go, 'unhide', 'after', 'DOWNLOAD_IMAGE_QUERY_DATA'); }
 
  
}
+
//<NOLITE>
 +
/** Test function for debugging preview problems one step at a time. */
 +
/*eslint-disable */
 +
function previewSteps(txt) {
 +
try {
 +
txt = txt || document.editform.wpTextbox1.value;
 +
} catch (err) {
 +
if (pg.cache.pages.length > 0) {
 +
txt = pg.cache.pages[pg.cache.pages.length - 1].data;
 +
} else {
 +
alert('provide text or use an edit page');
 +
}
 +
}
 +
txt = txt.substring(0, 10000);
 +
var base = pg.wiki.articlebase + Title.fromURL(document.location.href).urlString();
 +
var p = new Previewmaker(txt, base, pg.current.link.navpopup);
 +
if (this.owner.article.namespaceId() != pg.nsTemplateId) {
 +
p.killComments();
 +
if (!confirm('done killComments(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killDivs();
 +
if (!confirm('done killDivs(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killGalleries();
 +
if (!confirm('done killGalleries(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killBoxTemplates();
 +
if (!confirm('done killBoxTemplates(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
  
function popupsInsertImage(id, navpop, download) {
+
if (getValueOf('popupPreviewKillTemplates')) {
log( "popupsInsertImage");
+
p.killTemplates();
var imageinfo;
+
if (!confirm('done killTemplates(). Continue?\n---\n' + p.data)) {
try {
+
return;
var jsObj=getJsObj(download.data);
+
}
var imagepage=anyChild(jsObj.query.pages);
+
} else {
if (typeof imagepage.imageinfo === 'undefined') return;
+
p.killMultilineTemplates();
imageinfo = imagepage.imageinfo[0];
+
if (!confirm('done killMultilineTemplates(). Continue?\n---\n' + p.data)) {
} catch (someError) {
+
return;
log( "popupsInsertImage failed :(" );
+
}
return;
+
}
}
 
  
var popupImage = document.getElementById("popupImg"+id);
+
p.killTables();
if (!popupImage) {
+
if (!confirm('done killTables(). Continue?\n---\n' + p.data)) {
log( "could not find insertion point for image");
+
return;
return;
+
}
}
+
p.killImages();
 +
if (!confirm('done killImages(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killHTML();
 +
if (!confirm('done killHTML(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killChunks();
 +
if (!confirm('done killChunks(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.mopup();
 +
if (!confirm('done mopup(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
  
popupImage.width=getValueOf('popupImageSize');
+
p.firstBit();
popupImage.style.display='inline';
+
if (!confirm('done firstBit(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
p.killBadWhitespace();
 +
if (!confirm('done killBadWhitespace(). Continue?\n---\n' + p.data)) {
 +
return;
 +
}
 +
}
  
// Set the source for the image.
+
p.html = wiki2html(p.data, base); // needs livepreview
if( imageinfo.thumburl )
+
p.fixHTML();
popupImage.src=imageinfo.thumburl;
+
if (!confirm('done fixHTML(). Continue?\n---\n' + p.html)) {
else if( imageinfo.mime.indexOf("image") === 0 ){
+
return;
popupImage.src=imageinfo.url;
+
}
log( "a thumb could not be found, using original image" );
+
p.stripLongTemplates();
} else log( "fullsize imagethumb, but not sure if it's an image");
+
if (!confirm('done stripLongTemplates(). Continue?\n---\n' + p.html)) {
 
+
return;
 
 
var a=document.getElementById("popupImageLink"+id);
 
if (a === null) { return null; }
 
 
 
// Determine the action of the surrouding imagelink.
 
switch (getValueOf('popupThumbAction')) {
 
case 'imagepage':
 
if (pg.current.article.namespaceId()!=pg.nsImageId) {
 
a.href=imageinfo.descriptionurl;
 
// FIXME: unreliable pg.idNumber
 
popTipsSoonFn('popupImage' + id)();
 
break;
 
 
}
 
}
/* falls through */
+
alert('finished preview - end result follows.\n---\n' + p.html);
case 'sizetoggle':
 
a.onclick=toggleSize;
 
a.title=popupString('Toggle image size');
 
return;
 
case 'linkfull':
 
a.href = imageinfo.url;
 
a.title=popupString('Open full-size image');
 
return;
 
 
}
 
}
 +
/*eslint-enable */
 +
//</NOLITE>
  
}
+
/**
 +
* Works around livepreview bugs.
 +
* @private
 +
*/
 +
Previewmaker.prototype.fixHTML = function () {
 +
if (!this.html) return;
  
// Toggles the image between inline small and navpop fullwidth.
+
var ret = this.html;
// It's the same image, no actual sizechange occurs, only display width.
 
function toggleSize() {
 
var imgContainer=this;
 
if (!imgContainer) {
 
alert('imgContainer is null :/');
 
return;
 
}
 
img=imgContainer.firstChild;
 
if (!img) {
 
alert('img is null :/');
 
return;
 
}
 
  
if (!img.style.width || img.style.width==='') {
+
// fix question marks in wiki links
img.style.width='100%';
+
// maybe this'll break some stuff :-(
} else {
+
ret = ret.replace(
img.style.width='';
+
RegExp('(<a href="' + pg.wiki.articlePath + '/[^"]*)[?](.*?")', 'g'),
}
+
'$1%3F$2'
}
+
);
 +
ret = ret.replace(
 +
RegExp("(<a href='" + pg.wiki.articlePath + "/[^']*)[?](.*?')", 'g'),
 +
'$1%3F$2'
 +
);
 +
// FIXME fix up % too
  
// Returns one title of an image from wikiText.
+
this.html = ret;
function getValidImageFromWikiText(wikiText) {
+
};
// nb in pg.re.image we're interested in the second bracketed expression
 
// this may change if the regex changes :-(
 
//var match=pg.re.image.exec(wikiText);
 
var matched=null;
 
var match;
 
// strip html comments, used by evil bots :-(
 
var t = removeMatchesUnless(wikiText, RegExp('(<!--[\\s\\S]*?-->)'), 1,
 
RegExp('^<!--[^[]*popup', 'i'));
 
  
while ( ( match = pg.re.image.exec(t) ) ) {
+
/**
// now find a sane image name - exclude templates by seeking {
+
* Generates the preview and displays it in the current popup.
var m = match[2] || match[6];
 
if ( isValidImageName(m) ) {
 
matched=m;
 
break;
 
}
 
}
 
pg.re.image.lastIndex=0;
 
if (!matched) { return null; }
 
return mw.config.get('wgFormattedNamespaces')[pg.nsImageId]+':'+upcaseFirst(matched);
 
}
 
  
function removeMatchesUnless(str, re1, parencount, re2) {
+
* Does nothing if the generated preview is invalid or consists of whitespace only.
var split=str.parenSplit(re1);
+
* Also activates wikilinks in the preview for subpopups if the popupSubpopups option is true.
var c=parencount + 1;
+
*/
for (var i=0; i<split.length; ++i) {
+
Previewmaker.prototype.showPreview = function () {
if ( i%c === 0 || re2.test(split[i]) ) { continue; }
+
this.makePreview();
split[i]='';
+
if (typeof this.html != typeof '') return;
}
+
if (RegExp('^\\s*$').test(this.html)) return;
return split.join('');
+
setPopupHTML('<hr />', 'popupPrePreviewSep', this.owner.idNumber);
}
+
setPopupTipsAndHTML(this.html, 'popupPreview', this.owner.idNumber, {
 +
owner: this.owner,
 +
});
 +
var more = this.fullLength > this.data.length ? this.moreLink() : '';
 +
setPopupHTML(more, 'popupPreviewMore', this.owner.idNumber);
 +
};
  
//</NOLITE>
+
/**
// ENDFILE: images.js
+
* @private
// STARTFILE: namespaces.js
+
*/
// Set up namespaces and other non-strings.js localization
+
Previewmaker.prototype.moreLink = function () {
// (currently that means redirs too)
+
var a = document.createElement('a');
 +
a.className = 'popupMoreLink';
 +
a.innerHTML = popupString('more...');
 +
var savedThis = this;
 +
a.onclick = function () {
 +
savedThis.maxCharacters += 2000;
 +
savedThis.maxSentences += 20;
 +
savedThis.setData();
 +
savedThis.showPreview();
 +
};
 +
return a;
 +
};
  
 +
/**
 +
* @private
 +
*/
 +
Previewmaker.prototype.stripLongTemplates = function () {
 +
// operates on the HTML!
 +
this.html = this.html.replace(
 +
RegExp('^.{0,1000}[{][{][^}]*?(<(p|br)( /)?>\\s*){2,}([^{}]*?[}][}])?', 'gi'),
 +
''
 +
);
 +
this.html = this.html.split('\n').join(' '); // workaround for <pre> templates
 +
this.html = this.html.replace(RegExp('[{][{][^}]*<pre>[^}]*[}][}]', 'gi'), '');
 +
};
  
function namespaceListToRegex(list) {
+
/**
return RegExp('^('+list.join('|').split(' ').join('[ _]')+'):');
+
* @private
}
+
*/
 
+
Previewmaker.prototype.killMultilineTemplates = function () {
function setNamespaces() {
+
this.kill('{{{', '}}}');
pg.nsSpecialId  = -1;
+
this.kill(RegExp('\\s*[{][{][^{}]*\\n'), '}}', '{{');
pg.nsMainspaceId = 0;
 
pg.nsImageId    = 6;
 
pg.nsUserId      = 2;
 
pg.nsUsertalkId  = 3;
 
pg.nsCategoryId  = 14;
 
pg.nsTemplateId  = 10;
 
}
 
 
 
 
 
function setRedirs() {
 
var r='redirect';
 
var R='REDIRECT';
 
var redirLists={
 
//<NOLITE>
 
'ar':  [ R, 'تحويل' ],
 
'be':  [ r, 'перанакіраваньне' ],
 
'bg':  [ r, 'пренасочване', 'виж' ],
 
'bs':  [ r, 'Preusmjeri', 'preusmjeri', 'PREUSMJERI' ],
 
'cs':  [ R, 'PŘESMĚRUJ' ],
 
'cy':  [ r, 'ail-cyfeirio' ],
 
'de':  [ R, 'WEITERLEITUNG' ],
 
'el':  [ R, 'ΑΝΑΚΑΤΕΥΘΥΝΣΗ'],
 
'eo':  [ R, 'ALIDIREKTU', 'ALIDIREKTI' ],
 
'es':  [ R, 'REDIRECCIÓN' ],
 
'et':  [ r, 'suuna' ],
 
'ga':  [ r, 'athsheoladh' ],
 
'gl':  [ r, 'REDIRECCIÓN', 'REDIRECIONAMENTO'],
 
'he':  [ R, 'הפניה' ],
 
'hu':  [ R, 'ÁTIRÁNYÍTÁS' ],
 
'is':  [ r, 'tilvísun', 'TILVÍSUN' ],
 
'it':  [ R, 'RINVIA', 'Rinvia'],
 
'ja':  [ R, '転送' ],
 
'mk':  [ r, 'пренасочување', 'види' ],
 
'nds': [ r, 'wiederleiden' ],
 
'nl':  [ R, 'DOORVERWIJZING' ],
 
'nn':  [ r, 'omdiriger' ],
 
'pl':  [ R, 'PATRZ', 'PRZEKIERUJ', 'TAM' ],
 
'pt':  [ R, 'redir' ],
 
'ru':  [ R, 'ПЕРЕНАПРАВЛЕНИЕ', 'ПЕРЕНАПР' ],
 
'sk':  [ r, 'presmeruj' ],
 
'sr':  [ r, 'Преусмери', 'преусмери', 'ПРЕУСМЕРИ', 'Preusmeri', 'preusmeri', 'PREUSMERI' ],
 
'tt':  [ R, 'yünältü', 'перенаправление', 'перенапр' ],
 
'uk':  [ R, 'ПЕРЕНАПРАВЛЕННЯ', 'ПЕРЕНАПР' ],
 
'vi':  [ r, 'đổi' ],
 
'zh':  [ R, '重定向'] // no comma
 
//</NOLITE>
 
 
};
 
};
var redirList=redirLists[ pg.wiki.lang ] || [r, R];
+
// ENDFILE: previewmaker.js
// Mediawiki is very tolerant about what comes after the #redirect at the start
 
pg.re.redirect=RegExp('^\\s*[#](' + redirList.join('|') + ').*?\\[{2}([^\\|\\]]*)(|[^\\]]*)?\\]{2}\\s*(.*)', 'i');
 
}
 
  
function setInterwiki() {
+
// STARTFILE: querypreview.js
if (pg.wiki.wikimedia) {
+
function loadAPIPreview(queryType, article, navpop) {
// From http://meta.wikimedia.org/wiki/List_of_Wikipedias
+
var art = new Title(article).urlString();
pg.wiki.interwiki='aa|ab|ace|af|ak|als|am|an|ang|ar|arc|arz|as|ast|av|ay|az|ba|bar|bat-smg|bcl|be|be-x-old|bg|bh|bi|bjn|bm|bn|bo|bpy|br|bs|bug|bxr|ca|cbk-zam|cdo|ce|ceb|ch|cho|chr|chy|ckb|co|cr|crh|cs|csb|cu|cv|cy|da|de|diq|dsb|dv|dz|ee|el|eml|en|eo|es|et|eu|ext|fa|ff|fi|fiu-vro|fj|fo|fr|frp|frr|fur|fy|ga|gag|gan|gd|gl|glk|gn|got|gu|gv|ha|hak|haw|he|hi|hif|ho|hr|hsb|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|ilo|io|is|it|iu|ja|jbo|jv|ka|kaa|kab|kbd|kg|ki|kj|kk|kl|km|kn|ko|koi|kr|krc|ks|ksh|ku|kv|kw|ky|la|lad|lb|lbe|lg|li|lij|lmo|ln|lo|lt|ltg|lv|map-bms|mdf|mg|mh|mhr|mi|mk|ml|mn|mo|mr|mrj|ms|mt|mus|mwl|my|myv|mzn|na|nah|nap|nds|nds-nl|ne|new|ng|nl|nn|no|nov|nrm|nv|ny|oc|om|or|os|pa|pag|pam|pap|pcd|pdc|pfl|pi|pih|pl|pms|pnb|pnt|ps|pt|qu|rm|rmy|rn|ro|roa-rup|roa-tara|ru|rue|rw|sa|sah|sc|scn|sco|sd|se|sg|sh|si|simple|sk|sl|sm|sn|so|sq|sr|srn|ss|st|stq|su|sv|sw|szl|ta|te|tet|tg|th|ti|tk|tl|tn|to|tpi|tr|ts|tt|tum|tw|ty|udm|ug|uk|ur|uz|ve|vec|vi|vls|vo|wa|war|wo|wuu|xal|xh|yi|yo|za|zea|zh|zh-classical|zh-min-nan|zh-yue|zu';
+
var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';
pg.re.interwiki=RegExp('^'+pg.wiki.interwiki+':');
+
var htmlGenerator = function (/*a, d*/) {
} else {
+
alert('invalid html generator');
pg.wiki.interwiki=null;
+
};
pg.re.interwiki=RegExp('^$');
+
var usernameart = '';
}
+
switch (queryType) {
}
+
case 'history':
 
+
url +=
// return a regexp pattern matching all variants to write the given namespace
+
'titles=' + art + '&prop=revisions&rvlimit=' + getValueOf('popupHistoryPreviewLimit');
function nsRe(namespaceId) {
+
htmlGenerator = APIhistoryPreviewHTML;
var imageNamespaceVariants = [];
+
break;
jQuery.each(mw.config.get('wgNamespaceIds'), function(_localizedNamespaceLc, _namespaceId) {
+
case 'category':
if (_namespaceId!=namespaceId) return;
+
url += 'list=categorymembers&cmtitle=' + art;
//todo: escape regexp fragments!
+
htmlGenerator = APIcategoryPreviewHTML;
_localizedNamespaceLc = upcaseFirst(_localizedNamespaceLc);
+
break;
imageNamespaceVariants.push(_localizedNamespaceLc.split(' ').join('[ _]'));
+
case 'userinfo':
imageNamespaceVariants.push(encodeURI(_localizedNamespaceLc));
+
var username = new Title(article).userName();
});
+
usernameart = encodeURIComponent(username);
 
+
if (pg.re.ipUser.test(username)) {
return '(?:' + imageNamespaceVariants.join('|') + ')';
+
url += 'list=blocks&bkprop=range|restrictions&bkip=' + usernameart;
}
+
} else {
 
+
url +=
function nsReImage() {
+
'list=users|usercontribs&usprop=blockinfo|groups|editcount|registration|gender&ususers=' +
return nsRe(pg.nsImageId);
+
usernameart +
}
+
'&meta=globaluserinfo&guiprop=groups|unattached&guiuser=' +
// ENDFILE: namespaces.js
+
usernameart +
// STARTFILE: selpop.js
+
'&uclimit=1&ucprop=timestamp&ucuser=' +
//<NOLITE>
+
usernameart;
function getEditboxSelection() {
+
}
// see http://www.webgurusforum.com/8/12/0
+
htmlGenerator = APIuserInfoPreviewHTML;
var editbox;
+
break;
try {
+
case 'contribs':
editbox=document.editform.wpTextbox1;
+
usernameart = encodeURIComponent(new Title(article).userName());
} catch (dang) { return; }
+
url +=
// IE, Opera
+
'list=usercontribs&ucuser=' +
if (document.selection) { return document.selection.createRange().text; }
+
usernameart +
// Mozilla
+
'&uclimit=' +
var selStart = editbox.selectionStart;
+
getValueOf('popupContribsPreviewLimit');
var selEnd = editbox.selectionEnd;
+
htmlGenerator = APIcontribsPreviewHTML;
return (editbox.value).substring(selStart, selEnd);
+
break;
}
+
case 'imagepagepreview':
 
+
var trail = '';
function doSelectionPopup() {
+
if (getValueOf('popupImageLinks')) {
// popup if the selection looks like [[foo|anything afterwards at all
+
trail = '&list=imageusage&iutitle=' + art;
// or [[foo|bar]]text without ']]'
+
}
// or [[foo|bar]]
+
url += 'titles=' + art + '&prop=revisions|imageinfo&rvprop=content' + trail;
var sel=getEditboxSelection();
+
htmlGenerator = APIimagepagePreviewHTML;
var open=sel.indexOf('[[');
+
break;
var pipe=sel.indexOf('|');
+
case 'backlinks':
var close=sel.indexOf(']]');
+
url += 'list=backlinks&bltitle=' + art;
if (open == -1 || ( pipe == -1 && close == -1) ) { return; }
+
htmlGenerator = APIbacklinksPreviewHTML;
if (pipe != -1 && open > pipe || close != -1 && open > close) { return; }
+
break;
if (getValueOf('popupOnEditSelection')=='boxpreview') {
+
case 'revision':
return doSeparateSelectionPopup(sel);
+
if (article.oldid) {
}
+
url += 'revids=' + article.oldid;
var article=new Title(sel.substring(open+2, (pipe < 0) ? close : pipe)).urlString();
+
} else {
if (close > 0 && sel.substring(close+2).indexOf('[[') >= 0) {
+
url += 'titles=' + article.removeAnchor().urlString();
return;  
+
}
}
+
url +=
var a=document.createElement('a');
+
'&prop=revisions|pageprops|info|images|categories&rvprop=ids|timestamp|flags|comment|user|content&cllimit=max&imlimit=max';
a.href=pg.wiki.titlebase + article;
+
htmlGenerator = APIrevisionPreviewHTML;
mouseOverWikiLink2(a);
+
break;
if (a.navpopup) {
+
}
a.navpopup.addHook(function(){runStopPopupTimer(a.navpopup);}, 'unhide', 'after');
+
pendingNavpopTask(navpop);
 +
var callback = function (d) {
 +
log('callback of API functions was hit');
 +
if (queryType === 'userinfo') {
 +
// We need to do another API request
 +
fetchUserGroupNames(d.data).then(function () {
 +
showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
 +
});
 +
return;
 +
}
 +
showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
 +
};
 +
var go = function () {
 +
getPageWithCaching(url, callback, navpop);
 +
return true;
 +
};
 +
 
 +
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
 +
go();
 +
} else {
 +
navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_' + queryType + '_QUERY_DATA');
 +
}
 
}
 
}
}
 
  
function doSeparateSelectionPopup(str) {
+
function linkList(list) {
var div=document.getElementById('selectionPreview');
+
list.sort(function (x, y) {
if (!div) {
+
return x == y ? 0 : x < y ? -1 : 1;
div = document.createElement('div');
+
});
div.id='selectionPreview';
+
var buf = [];
try {
+
for (var i = 0; i < list.length; ++i) {
var box=document.editform.wpTextbox1;
+
buf.push(
box.parentNode.insertBefore(div, box);
+
wikiLink({
} catch (error) {
+
article: new Title(list[i]),
return;
+
text: list[i].split(' ').join('&nbsp;'),
 +
action: 'view',
 +
})
 +
);
 
}
 
}
 +
return buf.join(', ');
 
}
 
}
div.innerHTML=wiki2html(str);
 
div.ranSetupTooltipsAlready = false;
 
popTipsSoonFn('selectionPreview')();
 
}
 
//</NOLITE>
 
// ENDFILE: selpop.js
 
// STARTFILE: navpopup.js
 
/**
 
  @fileoverview  Defines two classes: {@link Navpopup} and {@link Mousetracker}.
 
  
  <code>Navpopup</code> describes popups: when they appear, where, what
+
function getTimeOffset() {
  they look like and so on.
+
var tz = mw.user.options.get('timecorrection');
  
  <code>Mousetracker</code> "captures" the mouse using
+
if (tz) {
  <code>document.onmousemove</code>.
+
if (tz.indexOf('|') > -1) {
*/
+
// New format
 +
return parseInt(tz.split('|')[1], 10);
 +
}
 +
}
 +
return 0;
 +
}
  
 +
function getTimeZone() {
 +
if (!pg.user.timeZone) {
 +
var tz = mw.user.options.get('timecorrection');
 +
pg.user.timeZone = 'UTC';
  
/**
+
if (tz) {
  Creates a new Mousetracker.
+
var tzComponents = tz.split('|');
  @constructor
+
if (tzComponents.length === 3 && tzComponents[0] === 'ZoneInfo') {
  @class The Mousetracker class. This monitors mouse movements and manages associated hooks.
+
pg.user.timeZone = tzComponents[2];
*/
+
} else {
function Mousetracker() {
+
errlog('Unexpected timezone information: ' + tz);
/**
+
}
  Interval to regularly run the hooks anyway, in milliseconds.
+
}
  @type Integer
+
}
*/
+
return pg.user.timeZone;
this.loopDelay=400;
+
}
  
 
/**
 
/**
  Timer for the loop.
+
* Should we use an offset or can we use proper timezones
  @type Timer
+
*/
*/
+
function useTimeOffset() {
this.timer=null;
+
if (typeof Intl.DateTimeFormat.prototype.formatToParts === 'undefined') {
 +
// IE 11
 +
return true;
 +
}
 +
var tz = mw.user.options.get('timecorrection');
 +
if (tz && tz.indexOf('ZoneInfo|') === -1) {
 +
// System| Default system time, default for users who didn't configure timezone
 +
// Offset| Manual defined offset by user
 +
return true;
 +
}
 +
return false;
 +
}
  
 
/**
 
/**
  Flag - are we switched on?
+
* Array of locales for the purpose of javascript locale based formatting
  @type Boolean
+
* Filters down to those supported by the browser. Empty [] === System's default locale
*/
+
*/
this.active=false;
+
function getLocales() {
 +
if (!pg.user.locales) {
 +
var userLanguage = document.querySelector('html').getAttribute('lang'); // make sure we have HTML locale
 +
if (getValueOf('popupLocale')) {
 +
userLanguage = getValueOf('popupLocale');
 +
} else if (userLanguage === 'en') {
 +
// en.wp tends to treat this as international english / unspecified
 +
// but we have more specific settings in user options
 +
if (getMWDateFormat() === 'mdy') {
 +
userLanguage = 'en-US';
 +
} else {
 +
userLanguage = 'en-GB';
 +
}
 +
}
 +
pg.user.locales = Intl.DateTimeFormat.supportedLocalesOf([userLanguage, navigator.language]);
 +
}
 +
return pg.user.locales;
 +
}
 +
 
 
/**
 
/**
  Flag - are we probably inaccurate, i.e. not reflecting the actual mouse position?
+
* Retrieve configured MW date format for this user
*/
+
* These can be
this.dirty=true;
+
* default
 +
* dmy: time, dmy
 +
* mdy: time, mdy
 +
* ymd: time, ymd
 +
* dmyt: dmy, time
 +
* dmyts: dmy, time + seconds
 +
* ISO 8601: YYYY-MM-DDThh:mm:ss (local time)
 +
*
 +
* This isn't too useful for us, as JS doesn't have formatters to match these private specifiers
 +
*/
 +
function getMWDateFormat() {
 +
return mw.user.options.get('date');
 +
}
 +
 
 
/**
 
/**
  Array of hook functions.
+
* Creates a HTML table that's shown in the history and user-contribs popups.
  @private
+
* @param {Object[]} h - a list of revisions, returned from the API
  @type Array
+
* @param {boolean} reallyContribs - true only if we're displaying user contributions
*/
+
*/
this.hooks=[];
+
function editPreviewTable(article, h, reallyContribs) {
}
+
var html = ['<table>'];
 +
var day = null;
 +
var curart = article;
 +
var page = null;
  
/**
+
var makeFirstColumnLinks;
  Adds a hook, to be called when we get events.
+
if (reallyContribs) {
  @param {Function} f A function which is called as
+
// We're showing user contributions, so make (diff | hist) links
  <code>f(x,y)</code>. It should return <code>true</code> when it
+
makeFirstColumnLinks = function (currentRevision) {
  wants to be removed, and <code>false</code> otherwise.
+
var result = '(';
*/
+
result +=
Mousetracker.prototype.addHook = function (f) {
+
'<a href="' +
this.hooks.push(f);
+
pg.wiki.titlebase +
};
+
new Title(currentRevision.title).urlString() +
 +
'&diff=prev' +
 +
'&oldid=' +
 +
currentRevision.revid +
 +
'">' +
 +
popupString('diff') +
 +
'</a>';
 +
result += '&nbsp;|&nbsp;';
 +
result +=
 +
'<a href="' +
 +
pg.wiki.titlebase +
 +
new Title(currentRevision.title).urlString() +
 +
'&action=history">' +
 +
popupString('hist') +
 +
'</a>';
 +
result += ')';
 +
return result;
 +
};
 +
} else {
 +
// It's a regular history page, so make (cur | last) links
 +
var firstRevid = h[0].revid;
 +
makeFirstColumnLinks = function (currentRevision) {
 +
var result = '(';
 +
result +=
 +
'<a href="' +
 +
pg.wiki.titlebase +
 +
new Title(curart).urlString() +
 +
'&diff=' +
 +
firstRevid +
 +
'&oldid=' +
 +
currentRevision.revid +
 +
'">' +
 +
popupString('cur') +
 +
'</a>';
 +
result += '&nbsp;|&nbsp;';
 +
result +=
 +
'<a href="' +
 +
pg.wiki.titlebase +
 +
new Title(curart).urlString() +
 +
'&diff=prev&oldid=' +
 +
currentRevision.revid +
 +
'">' +
 +
popupString('last') +
 +
'</a>';
 +
result += ')';
 +
return result;
 +
};
 +
}
  
/**
+
for (var i = 0; i < h.length; ++i) {
  Runs hooks, passing them the x
+
if (reallyContribs) {
  and y coords of the mouse.  Hook functions that return true are
+
page = h[i].title;
  passed to {@link Mousetracker#removeHooks} for removal.
+
curart = new Title(page);
  @private
+
}
*/
+
var minor = h[i].minor ? '<b>m </b>' : '';
Mousetracker.prototype.runHooks = function () {
+
var editDate = new Date(h[i].timestamp);
if (!this.hooks || !this.hooks.length) { return; }
+
var thisDay = formattedDate(editDate);
//log('Mousetracker.runHooks; we got some hooks to run');
+
var thisTime = formattedTime(editDate);
var remove=false;
+
if (thisDay == day) {
var removeObj={};
+
thisDay = '';
// this method gets called a LOT -
+
} else {
// pre-cache some variables
+
day = thisDay;
var x=this.x, y=this.y, len = this.hooks.length;
+
}
 
+
if (thisDay) {
for (var i=0; i<len; ++i) {
+
html.push(
//~ run the hook function, and remove it if it returns true
+
'<tr><td colspan=3><span class="popup_history_date">' + thisDay + '</span></td></tr>'
if (this.hooks[i](x, y)===true) {
+
);
remove=true;
+
}
removeObj[i]=true;
+
html.push('<tr class="popup_history_row_' + (i % 2 ? 'odd' : 'even') + '">');
 +
html.push('<td>' + makeFirstColumnLinks(h[i]) + '</td>');
 +
html.push(
 +
'<td>' +
 +
'<a href="' +
 +
pg.wiki.titlebase +
 +
new Title(curart).urlString() +
 +
'&oldid=' +
 +
h[i].revid +
 +
'">' +
 +
thisTime +
 +
'</a></td>'
 +
);
 +
var col3url = '',
 +
col3txt = '';
 +
if (!reallyContribs) {
 +
var user = h[i].user;
 +
if (!h[i].userhidden) {
 +
if (pg.re.ipUser.test(user)) {
 +
col3url =
 +
pg.wiki.titlebase +
 +
mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +
 +
':Contributions&target=' +
 +
new Title(user).urlString();
 +
} else {
 +
col3url =
 +
pg.wiki.titlebase +
 +
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] +
 +
':' +
 +
new Title(user).urlString();
 +
}
 +
col3txt = pg.escapeQuotesHTML(user);
 +
} else {
 +
col3url = getValueOf('popupRevDelUrl');
 +
col3txt = pg.escapeQuotesHTML(popupString('revdel'));
 +
}
 +
} else {
 +
col3url = pg.wiki.titlebase + curart.urlString();
 +
col3txt = pg.escapeQuotesHTML(page);
 +
}
 +
html.push(
 +
'<td>' +
 +
(reallyContribs ? minor : '') +
 +
'<a href="' +
 +
col3url +
 +
'">' +
 +
col3txt +
 +
'</a></td>'
 +
);
 +
var comment = '';
 +
var c = h[i].comment || h[i].content;
 +
if (c) {
 +
comment = new Previewmaker(c, new Title(curart).toUrl()).editSummaryPreview();
 +
} else if (h[i].commenthidden) {
 +
comment = popupString('revdel');
 +
}
 +
html.push('<td>' + (!reallyContribs ? minor : '') + comment + '</td>');
 +
html.push('</tr>');
 +
html = [html.join('')];
 
}
 
}
 +
html.push('</table>');
 +
return html.join('');
 
}
 
}
if (remove) { this.removeHooks(removeObj); }
 
};
 
  
/**
+
function adjustDate(d, offset) {
  Removes hooks.
+
// offset is in minutes
  @private
+
var o = offset * 60 * 1000;
  @param {Object} removeObj An object whose keys are the index
+
return new Date(+d + o);
  numbers of functions for removal, with values that evaluate to true
 
*/
 
Mousetracker.prototype.removeHooks = function(removeObj) {
 
var newHooks=[];
 
var len = this.hooks.length;
 
for (var i=0; i<len; ++i) {
 
if (! removeObj[i]) { newHooks.push(this.hooks[i]); }
 
 
}
 
}
this.hooks=newHooks;
 
};
 
  
 +
/**
 +
* This relies on the Date parser understanding en-US dates,
 +
* which is pretty safe assumption, but not perfect.
 +
*/
 +
function convertTimeZone(date, timeZone) {
 +
return new Date(date.toLocaleString('en-US', { timeZone: timeZone }));
 +
}
  
/**
+
function formattedDateTime(date) {
  Event handler for mouse wiggles.
+
// fallback for IE11 and unknown timezones
  We simply grab the event, set x and y and run the hooks.
+
if (useTimeOffset()) {
  This makes the cpu all hot and bothered :-(
+
return formattedDate(date) + ' ' + formattedTime(date);
  @private
+
}
  @param {Event} e Mousemove event
 
*/
 
Mousetracker.prototype.track=function (e) {
 
//~ Apparently this is needed in IE.
 
e = e || window.event;
 
var x, y;
 
if (e) {
 
if (e.pageX) { x=e.pageX; y=e.pageY; }
 
else if (typeof e.clientX!='undefined') {
 
var left, top, docElt = document.documentElement;
 
  
if (docElt) { left=docElt.scrollLeft; }
+
if (getMWDateFormat() === 'ISO 8601') {
left = left || document.body.scrollLeft || document.scrollLeft || 0;
+
var d2 = convertTimeZone(date, getTimeZone());
 +
return (
 +
map(zeroFill, [d2.getFullYear(), d2.getMonth() + 1, d2.getDate()]).join('-') +
 +
'T' +
 +
map(zeroFill, [d2.getHours(), d2.getMinutes(), d2.getSeconds()]).join(':')
 +
);
 +
}
  
if (docElt) { top=docElt.scrollTop; }
+
var options = getValueOf('popupDateTimeFormatterOptions');
top = top || document.body.scrollTop || document.scrollTop || 0;
+
options['timeZone'] = getTimeZone();
 +
return date.toLocaleString(getLocales(), options);
 +
}
  
x=e.clientX + left;
+
function formattedDate(date) {
y=e.clientY + top;
+
// fallback for IE11 and unknown timezones
} else { return; }
+
if (useTimeOffset()) {
this.setPosition(x,y);
+
// we adjust the UTC time, so we print the adjusted UTC, but not really UTC values
}
+
var d2 = adjustDate(date, getTimeOffset());
};
+
return map(zeroFill, [d2.getUTCFullYear(), d2.getUTCMonth() + 1, d2.getUTCDate()]).join('-');
 +
}
  
/**
+
if (getMWDateFormat() === 'ISO 8601') {
  Sets the x and y coordinates stored and takes appropriate action,
+
var d2 = convertTimeZone(date, getTimeZone());
  running hooks as appropriate.
+
return map(zeroFill, [d2.getFullYear(), d2.getMonth() + 1, d2.getDate()]).join('-');
  @param {Integer} x, y Screen coordinates to set
+
}
*/
 
  
Mousetracker.prototype.setPosition=function(x,y) {
+
var options = getValueOf('popupDateFormatterOptions');
this.x = x;
+
options['timeZone'] = getTimeZone();
this.y = y;
+
return date.toLocaleDateString(getLocales(), options);
if (this.dirty || this.hooks.length === 0) { this.dirty=false; return; }
 
if (typeof this.lastHook_x != 'number') { this.lastHook_x = -100; this.lastHook_y=-100; }
 
var diff = (this.lastHook_x - x)*(this.lastHook_y - y);
 
diff = (diff >= 0) ? diff : -diff;
 
if ( diff > 1 ) {
 
this.lastHook_x=x;
 
this.lastHook_y=y;
 
if (this.dirty) { this.dirty = false; }
 
else { this.runHooks(); }
 
 
}
 
}
};
 
  
/**
+
function formattedTime(date) {
  Sets things in motion, unless they are already that is, registering an event handler on <code>document.onmousemove</code>.
+
// fallback for IE11 and unknown timezones
  A half-hearted attempt is made to preserve the old event handler if there is one.
+
if (useTimeOffset()) {
*/
+
// we adjust the UTC time, so we print the adjusted UTC, but not really UTC values
Mousetracker.prototype.enable = function () {
+
var d2 = adjustDate(date, getTimeOffset());
if (this.active) { return; }
+
return map(zeroFill, [d2.getUTCHours(), d2.getUTCMinutes(), d2.getUTCSeconds()]).join(':');
this.active=true;
+
}
//~ Save the current handler for mousemove events. This isn't too
 
//~ robust, of course.
 
this.savedHandler=document.onmousemove;
 
//~ Gotta save @tt{this} again for the closure, and use apply for
 
//~ the member function.
 
var savedThis=this;
 
document.onmousemove=function (e) {savedThis.track.apply(savedThis, [e]);};
 
if (this.loopDelay) { this.timer = setInterval(function() { //log('loop delay in mousetracker is working');
 
savedThis.runHooks();}, this.loopDelay); }
 
};
 
  
/**
+
if (getMWDateFormat() === 'ISO 8601') {
  Disables the tracker, removing the event handler.
+
var d2 = convertTimeZone(date, getTimeZone());
*/
+
return map(zeroFill, [d2.getHours(), d2.getMinutes(), d2.getSeconds()]).join(':');
Mousetracker.prototype.disable = function () {
+
}
if (!this.active) { return; }
 
if ($.isFunction(this.savedHandler)) {
 
document.onmousemove=this.savedHandler;
 
} else { delete document.onmousemove; }
 
if (this.timer) { clearInterval(this.timer); }
 
this.active=false;
 
};
 
 
 
/**
 
  Creates a new Navpopup.
 
  Gets a UID for the popup and
 
  @param init Contructor object. If <code>init.draggable</code> is true or absent, the popup becomes draggable.
 
  @constructor
 
  @class The Navpopup class. This generates popup hints, and does some management of them.
 
*/
 
function Navpopup(init) {
 
//alert('new Navpopup(init)');
 
/** UID for each Navpopup instance.
 
Read-only.
 
@type integer
 
*/
 
this.uid=Navpopup.uid++;
 
/**
 
  Read-only flag for current visibility of the popup.
 
  @type boolean
 
  @private
 
*/
 
this.visible=false;
 
/** Flag to be set when we want to cancel a previous request to
 
show the popup in a little while.
 
@private
 
@type boolean
 
*/
 
this.noshow=false;
 
/** Categorised list of hooks.
 
@see #runHooks
 
@see #addHook
 
@private
 
@type Object
 
*/
 
this.hooks={
 
'create': [],
 
'unhide': [],
 
'hide': []
 
};
 
/** list of unique IDs of hook functions, to avoid duplicates
 
@private
 
*/
 
this.hookIds={};
 
/** List of downloads associated with the popup.
 
@private
 
@type Array
 
*/
 
this.downloads=[];
 
/** Number of uncompleted downloads.
 
@type integer
 
*/
 
this.pending=null;
 
/** Tolerance in pixels when detecting whether the mouse has left the popup.
 
@type integer
 
*/
 
this.fuzz=5;
 
/** Flag to toggle running {@link #limitHorizontalPosition} to regulate the popup's position.
 
@type boolean
 
*/
 
this.constrained=true;
 
/** The popup width in pixels.
 
@private
 
@type integer
 
*/
 
this.width=0;
 
/** The popup width in pixels.
 
@private
 
@type integer
 
*/
 
this.height=0;
 
/** The main content DIV element.
 
@type HTMLDivElement
 
*/
 
this.mainDiv=null;
 
this.createMainDiv();
 
  
// if (!init || typeof init.popups_draggable=='undefined' || init.popups_draggable) {
+
var options = getValueOf('popupTimeFormatterOptions');
// this.makeDraggable(true);
+
options['timeZone'] = getTimeZone();
// }
+
return date.toLocaleTimeString(getLocales(), options);
}
 
 
 
/**
 
  A UID for each Navpopup. This constructor property is just a counter.
 
  @type integer
 
  @private
 
*/
 
Navpopup.uid=0;
 
 
 
/**
 
  Retrieves the {@link #visible} attribute, indicating whether the popup is currently visible.
 
  @type boolean
 
*/
 
Navpopup.prototype.isVisible=function() {
 
return this.visible;
 
};
 
 
 
/**
 
  Repositions popup using CSS style.
 
  @private
 
  @param {integer} x x-coordinate (px)
 
  @param {integer} y y-coordinate (px)
 
  @param {boolean} noLimitHor Don't call {@link #limitHorizontalPosition}
 
*/
 
Navpopup.prototype.reposition= function (x,y, noLimitHor) {
 
log ('reposition('+x+','+y+','+noLimitHor+')');
 
if (typeof x != 'undefined' && x !== null) { this.left=x; }
 
if (typeof y != 'undefined' && y !== null) { this.top=y; }
 
if (typeof this.left != 'undefined' && typeof this.top != 'undefined') {
 
this.mainDiv.style.left=this.left + 'px';
 
this.mainDiv.style.top=this.top + 'px';
 
 
}
 
}
if (!noLimitHor) { this.limitHorizontalPosition(); }
 
//console.log('navpop'+this.uid+' - (left,top)=(' + this.left + ',' + this.top + '), css=('
 
//+ this.mainDiv.style.left + ',' + this.mainDiv.style.top + ')');
 
};
 
  
/**
+
// Get the proper groupnames for the technicalgroups
  Prevents popups from being in silly locations. Hopefully.
+
function fetchUserGroupNames(userinfoResponse) {
  Should not be run if {@link #constrained} is true.
+
var queryObj = getJsObj(userinfoResponse).query;
  @private
+
var user = anyChild(queryObj.users);
*/
+
var messages = [];
Navpopup.prototype.limitHorizontalPosition=function() {
+
if (user.groups) {
if (!this.constrained || this.tooWide) { return; }
+
user.groups.forEach(function (groupName) {
this.updateDimensions();
+
if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'named'].indexOf(groupName) === -1) {
var x=this.left;
+
messages.push('group-' + groupName + '-member');
var w=this.width;
+
}
var cWidth=document.body.clientWidth;
+
});
 +
}
 +
if (queryObj.globaluserinfo && queryObj.globaluserinfo.groups) {
 +
queryObj.globaluserinfo.groups.forEach(function (groupName) {
 +
messages.push('group-' + groupName + '-member');
 +
});
 +
}
 +
return getMwApi().loadMessagesIfMissing(messages);
 +
}
  
 +
function showAPIPreview(queryType, html, id, navpop, download) {
 +
// DJ: done
 +
var target = 'popupPreview';
 +
completedNavpopTask(navpop);
  
// log('limitHorizontalPosition: x='+x+
+
switch (queryType) {
// ', this.left=' + this.left +
+
case 'imagelinks':
// ', this.width=' + this.width +
+
case 'category':
// ', cWidth=' + cWidth);
+
target = 'popupPostPreview';
 
+
break;
 
+
case 'userinfo':
if ( (x+w) >= cWidth ||
+
target = 'popupUserData';
( x > 0 &&
+
break;
this.maxWidth &&
+
case 'revision':
this.width < this.maxWidth &&
+
insertPreview(download);
this.height > this.width &&
+
return;
x > cWidth - this.maxWidth ) ) {
+
}
// This is a very nasty hack. There has to be a better way!
+
setPopupTipsAndHTML(html, target, id);
// We find the "natural" width of the div by positioning it at the far left
 
// then reset it so that it should be flush right (well, nearly)
 
this.mainDiv.style.left='-10000px';
 
this.mainDiv.style.width = this.maxWidth + 'px';
 
var naturalWidth=parseInt(this.mainDiv.offsetWidth, 10);
 
var newLeft=cWidth - naturalWidth - 1;
 
if (newLeft < 0) { newLeft = 0; this.tooWide=true; } // still unstable for really wide popups?
 
log ('limitHorizontalPosition: moving to ('+newLeft + ','+ this.top+');' + ' naturalWidth=' + naturalWidth + ', clientWidth=' + cWidth);
 
this.reposition(newLeft, null, true);
 
 
}
 
}
};
 
  
/**
+
function APIrevisionPreviewHTML(article, download) {
  Counter indicating the z-order of the "highest" popup.
+
try {
  We start the z-index at 1000 so that popups are above everything
+
var jsObj = getJsObj(download.data);
  else on the screen.
+
var page = anyChild(jsObj.query.pages);
  @private
+
if (page.missing) {
  @type integer
+
// TODO we need to fix this proper later on
*/
+
download.owner = null;
Navpopup.highest=1000;
+
return;
 
+
}
/**
+
var content =
  Brings popup to the top of the z-order.
+
page && page.revisions && page.revisions[0].contentmodel === 'wikitext'
  We increment the {@link #highest} property of the contructor here.
+
? page.revisions[0].content
  @private
+
: null;
*/
+
if (typeof content === 'string') {
Navpopup.prototype.raise = function () {
+
download.data = content;
this.mainDiv.style.zIndex=Navpopup.highest + 1;
+
download.lastModified = new Date(page.revisions[0].timestamp);
++Navpopup.highest;
+
}
};
+
} catch (someError) {
 
+
return 'Revision preview failed :(';
/**
 
  Shows the popup provided {@link #noshow} is not true.
 
  Updates the position, brings the popup to the top of the z-order and unhides it.
 
*/
 
Navpopup.prototype.show = function () {
 
//document.title+='s';
 
if (this.noshow) { return; }
 
//document.title+='t';
 
this.reposition();
 
this.raise();
 
this.unhide();
 
};
 
 
 
 
 
/**
 
  Runs the {@link #show} method in a little while, unless we're
 
  already visible.
 
  @param {integer} time Delay in milliseconds
 
  @see #showSoonIfStable
 
*/
 
Navpopup.prototype.showSoon = function (time) {
 
if (this.visible) { return; }
 
this.noshow=false;
 
//~ We have to save the value of @tt{this} so that the closure below
 
//~ works.
 
var savedThis=this;
 
//this.start_x = Navpopup.tracker.x;
 
//this.start_y = Navpopup.tracker.y;
 
setTimeout(function () {
 
if (Navpopup.tracker.active) {
 
savedThis.reposition.apply(savedThis, [Navpopup.tracker.x + 2, Navpopup.tracker.y + 2]);
 
 
}
 
}
//~ Have to use apply to invoke his member function here
+
}
savedThis.show.apply(savedThis, []);
 
}, time);
 
};
 
  
/**
+
function APIbacklinksPreviewHTML(article, download /*, navpop*/) {
  Checks to see if the mouse pointer has
+
try {
  stabilised (checking every <code>time</code>/2 milliseconds) and runs the
+
var jsObj = getJsObj(download.data);
  {@link #show} method if it has. This method makes {@link #showSoon} redundant.
+
var list = jsObj.query.backlinks;
  @param {integer} time The minimum time (ms) before the popup may be shown.
 
*/
 
Navpopup.prototype.showSoonIfStable = function (time) {
 
log ('showSoonIfStable, time='+time);
 
if (this.visible) { return; }
 
this.noshow = false;
 
  
//~ initialize these variables so that we never run @tt{show} after
+
var html = [];
//~ just half the time
+
if (!list) {
this.stable_x = -10000; this.stable_y = -10000;
+
return popupString('No backlinks found');
 
+
}
var stableShow = function() {
+
for (var i = 0; i < list.length; i++) {
log('stableShow called');
+
var t = new Title(list[i].title);
var new_x = Navpopup.tracker.x, new_y = Navpopup.tracker.y;
+
html.push(
var dx = savedThis.stable_x - new_x, dy = savedThis.stable_y - new_y;
+
'<a href="' + pg.wiki.titlebase + t.urlString() + '">' + t.toString().entify() + '</a>'
var fuzz2 = 0; // savedThis.fuzz * savedThis.fuzz;
+
);
//document.title += '[' + [savedThis.stable_x,new_x, savedThis.stable_y,new_y, dx, dy, fuzz2].join(',') + '] ';
+
}
if ( dx * dx <= fuzz2 && dy * dy <= fuzz2 ) {
+
html = html.join(', ');
log ('mouse is stable');
+
if (jsObj['continue'] && jsObj['continue'].blcontinue) {
clearInterval(savedThis.showSoonStableTimer);
+
html += popupString(' and more');
savedThis.reposition.apply(savedThis, [new_x + 2, new_y + 2]);
+
}
savedThis.show.apply(savedThis, []);
+
return html;
return;
+
} catch (someError) {
 +
return 'backlinksPreviewHTML went wonky';
 
}
 
}
savedThis.stable_x = new_x; savedThis.stable_y = new_y;
 
};
 
var savedThis = this;
 
this.showSoonStableTimer = setInterval(stableShow, time/2);
 
};
 
 
/**
 
  Makes the popup unhidable until we call {@link #unstick}.
 
*/
 
Navpopup.prototype.stick=function() {
 
this.noshow=false;
 
this.sticky=true;
 
};
 
 
/**
 
  Allows the popup to be hidden.
 
*/
 
Navpopup.prototype.unstick=function() {
 
this.sticky=false;
 
};
 
 
/**
 
  Sets the {@link #noshow} flag and hides the popup. This should be called
 
  when the mouse leaves the link before
 
  (or after) it's actually been displayed.
 
*/
 
Navpopup.prototype.banish = function () {
 
log ('banish called');
 
// hide and prevent showing with showSoon in the future
 
this.noshow=true;
 
if (this.showSoonStableTimer) {
 
log('clearing showSoonStableTimer');
 
clearInterval(this.showSoonStableTimer);
 
 
}
 
}
this.hide();
 
};
 
  
/**
+
pg.fn.APIsharedImagePagePreviewHTML = function APIsharedImagePagePreviewHTML(obj) {
  Runs hooks added with {@link #addHook}.
+
log('APIsharedImagePagePreviewHTML');
  @private
+
var popupid = obj.requestid;
  @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
+
if (obj.query && obj.query.pages) {
  @param {String} when Controls exactly when the hook is run: either 'before' or 'after'
+
var page = anyChild(obj.query.pages);
*/
+
var content =
Navpopup.prototype.runHooks = function (key, when) {
+
page && page.revisions && page.revisions[0].contentmodel === 'wikitext'
if (!this.hooks[key]) { return; }
+
? page.revisions[0].content
var keyHooks=this.hooks[key];
+
: null;
var len=keyHooks.length;
+
if (
for (var i=0; i< len; ++i) {
+
typeof content === 'string' &&
if (keyHooks[i] && keyHooks[i].when == when) {
+
pg &&
if (keyHooks[i].hook.apply(this, [])) {
+
pg.current &&
// remove the hook
+
pg.current.link &&
if (keyHooks[i].hookId) {
+
pg.current.link.navpopup
delete this.hookIds[keyHooks[i].hookId];
+
) {
}
+
/* Not entirely safe, but the best we can do */
keyHooks[i]=null;
+
var p = new Previewmaker(
 +
content,
 +
pg.current.link.navpopup.article,
 +
pg.current.link.navpopup
 +
);
 +
p.makePreview();
 +
setPopupHTML(p.html, 'popupSecondPreview', popupid);
 
}
 
}
 
}
 
}
}
+
};
};
+
 
 +
function APIimagepagePreviewHTML(article, download, navpop) {
 +
try {
 +
var jsObj = getJsObj(download.data);
 +
var page = anyChild(jsObj.query.pages);
 +
var content =
 +
page && page.revisions && page.revisions[0].contentmodel === 'wikitext'
 +
? page.revisions[0].content
 +
: null;
 +
var ret = '';
 +
var alt = '';
 +
try {
 +
alt = navpop.parentAnchor.childNodes[0].alt;
 +
} catch (e) {}
 +
if (alt) {
 +
ret = ret + '<hr /><b>' + popupString('Alt text:') + '</b> ' + pg.escapeQuotesHTML(alt);
 +
}
 +
if (typeof content === 'string') {
 +
var p = prepPreviewmaker(content, article, navpop);
 +
p.makePreview();
 +
if (p.html) {
 +
ret += '<hr />' + p.html;
 +
}
 +
if (getValueOf('popupSummaryData')) {
 +
var info = getPageInfo(content, download);
 +
log(info);
 +
setPopupTrailer(info, navpop.idNumber);
 +
}
 +
}
 +
if (page && page.imagerepository == 'shared') {
 +
var art = new Title(article);
 +
var encart = encodeURIComponent('File:' + art.stripNamespace());
 +
var shared_url =
 +
pg.wiki.apicommonsbase +
 +
'?format=json&formatversion=2' +
 +
'&callback=pg.fn.APIsharedImagePagePreviewHTML' +
 +
'&requestid=' +
 +
navpop.idNumber +
 +
'&action=query&prop=revisions&rvprop=content&titles=' +
 +
encart;
  
/**
+
ret =
  Adds a hook to the popup. Hook functions are run with <code>this</code> set to refer to the Navpopup instance, and no arguments.
+
ret +
  @param {Function} hook The hook function. Functions that return true are deleted.
+
'<hr />' +
  @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
+
popupString('Image from Commons') +
  @param {String} when Controls exactly when the hook is run: either 'before' or 'after'
+
': <a href="' +
  @param {String} uid A truthy string identifying the hook function; if it matches another hook in this position, it won't be added again.
+
pg.wiki.commonsbase +
*/
+
'?title=' +
Navpopup.prototype.addHook = function ( hook, key, when, uid ) {
+
encart +
when = when || 'after';
+
'">' +
if (!this.hooks[key]) { return; }
+
popupString('Description page') +
// if uid is specified, don't add duplicates
+
'</a>';
var hookId=null;
+
mw.loader.load(shared_url);
if (uid) {
+
}
hookId=[key,when,uid].join('|');
+
showAPIPreview(
if (this.hookIds[hookId]) {
+
'imagelinks',
return;
+
APIimagelinksPreviewHTML(article, download),
 +
navpop.idNumber,
 +
download
 +
);
 +
return ret;
 +
} catch (someError) {
 +
return 'API imagepage preview failed :(';
 
}
 
}
this.hookIds[hookId]=true;
 
 
}
 
}
this.hooks[key].push( {hook: hook, when: when, hookId: hookId} );
 
};
 
  
/**
+
function APIimagelinksPreviewHTML(article, download) {
  Creates the main DIV element, which contains all the actual popup content.
+
try {
  Runs hooks with key 'create'.
+
var jsobj = getJsObj(download.data);
  @private
+
var list = jsobj.query.imageusage;
*/
+
if (list) {
Navpopup.prototype.createMainDiv = function () {
+
var ret = [];
if (this.mainDiv) { return; }
+
for (var i = 0; i < list.length; i++) {
this.runHooks('create', 'before');
+
ret.push(list[i].title);
var mainDiv=document.createElement('div');
+
}
 
+
if (ret.length === 0) {
var savedThis=this;
+
return popupString('No image links found');
mainDiv.onclick=function(e) {savedThis.onclickHandler(e);};
+
}
mainDiv.className=(this.className) ? this.className : 'navpopup_maindiv';
+
return '<h2>' + popupString('File links') + '</h2>' + linkList(ret);
mainDiv.id=mainDiv.className + this.uid;
+
} else {
 
+
return popupString('No image links found');
mainDiv.style.position='absolute';
+
}
mainDiv.style.display='none';
+
} catch (someError) {
mainDiv.className='navpopup';
+
return 'Image links preview generation failed :(';
 
+
}
// easy access to javascript object through DOM functions
 
mainDiv.navpopup=this;
 
 
 
this.mainDiv=mainDiv;
 
document.body.appendChild(mainDiv);
 
this.runHooks('create', 'after');
 
};
 
/**
 
  Calls the {@link #raise} method.
 
  @private
 
*/
 
Navpopup.prototype.onclickHandler=function(e) {
 
this.raise();
 
};
 
/**
 
  Makes the popup draggable, using a {@link Drag} object.
 
  @private
 
*/
 
Navpopup.prototype.makeDraggable=function(handleName) {
 
if (!this.mainDiv) { this.createMainDiv(); }
 
var drag=new Drag();
 
if (!handleName) {
 
drag.startCondition=function(e) {
 
try { if (!e.shiftKey) { return false; } } catch (err) { return false; }
 
return true;
 
};
 
 
}
 
}
var dragHandle;
 
if (handleName) dragHandle = document.getElementById(handleName);
 
if (!dragHandle) dragHandle = this.mainDiv;
 
var np=this;
 
drag.endHook=function(x,y) {
 
Navpopup.tracker.dirty=true;
 
np.reposition(x,y);
 
};
 
drag.init(dragHandle,this.mainDiv);
 
};
 
  
/** Hides the popup using CSS. Runs hooks with key 'hide'.
+
function APIcategoryPreviewHTML(article, download) {
Sets {@link #visible} appropriately. {@link #banish} should be called externally instead of this method.
+
try {
 
+
var jsobj = getJsObj(download.data);
@private
+
var list = jsobj.query.categorymembers;
*/
+
var ret = [];
Navpopup.prototype.hide = function () {
+
for (var p = 0; p < list.length; p++) {
this.runHooks('hide', 'before');
+
ret.push(list[p].title);
this.abortDownloads();
+
}
if (this.sticky) { return; }
+
if (ret.length === 0) {
if (typeof this.visible != 'undefined' && this.visible) {
+
return popupString('Empty category');
this.mainDiv.style.display='none';
+
}
this.visible=false;
+
ret = '<h2>' + tprintf('Category members (%s shown)', [ret.length]) + '</h2>' + linkList(ret);
 +
if (jsobj['continue'] && jsobj['continue'].cmcontinue) {
 +
ret += popupString(' and more');
 +
}
 +
return ret;
 +
} catch (someError) {
 +
return 'Category preview failed :(';
 +
}
 
}
 
}
this.runHooks('hide', 'after');
 
};
 
  
/** Shows the popup using CSS. Runs hooks with key 'unhide'.
+
function APIuserInfoPreviewHTML(article, download) {
Sets {@link #visible} appropriately.   {@link #show} should be called externally instead of this method.
+
var ret = [];
@private
+
var queryobj = {};
*/
+
try {
Navpopup.prototype.unhide = function () {
+
queryobj = getJsObj(download.data).query;
this.runHooks('unhide', 'before');
+
} catch (someError) {
if (typeof this.visible != 'undefined' && !this.visible) {
+
return 'Userinfo preview failed :(';
this.mainDiv.style.display='inline';
+
}
this.visible=true;
+
 
}
+
var user = anyChild(queryobj.users);
this.runHooks('unhide', 'after');
+
if (user) {
};
+
var globaluserinfo = queryobj.globaluserinfo;
 +
if (user.invalid === '') {
 +
ret.push(popupString('Invalid user'));
 +
} else if (user.missing === '') {
 +
ret.push(popupString('Not a registered username'));
 +
}
 +
if (user.blockedby) {
 +
if (user.blockpartial) {
 +
ret.push('<b>' + popupString('Has blocks') + '</b>');
 +
} else {
 +
ret.push('<b>' + popupString('BLOCKED') + '</b>');
 +
}
 +
}
 +
if (globaluserinfo && ('locked' in globaluserinfo || 'hidden' in globaluserinfo)) {
 +
var lockedSulAccountIsAttachedToThis = true;
 +
for (var i = 0; globaluserinfo.unattached && i < globaluserinfo.unattached.length; i++) {
 +
if (globaluserinfo.unattached[i].wiki === mw.config.get('wgDBname')) {
 +
lockedSulAccountIsAttachedToThis = false;
 +
break;
 +
}
 +
}
 +
if (lockedSulAccountIsAttachedToThis) {
 +
if ('locked' in globaluserinfo) ret.push('<b><i>' + popupString('LOCKED') + '</i></b>');
 +
if ('hidden' in globaluserinfo) ret.push('<b><i>' + popupString('HIDDEN') + '</i></b>');
 +
}
 +
}
 +
if (getValueOf('popupShowGender') && user.gender) {
 +
switch (user.gender) {
 +
case 'male':
 +
ret.push(popupString('he/him') + ' · ');
 +
break;
 +
case 'female':
 +
ret.push(popupString('she/her') + ' · ');
 +
break;
 +
}
 +
}
 +
if (user.groups) {
 +
user.groups.forEach(function (groupName) {
 +
if (['*', 'user', 'autoconfirmed', 'extendedconfirmed', 'named'].indexOf(groupName) === -1) {
 +
ret.push(
 +
pg.escapeQuotesHTML(mw.message('group-' + groupName + '-member', user.gender).text())
 +
);
 +
}
 +
});
 +
}
 +
if (globaluserinfo && globaluserinfo.groups) {
 +
globaluserinfo.groups.forEach(function (groupName) {
 +
ret.push(
 +
'<i>' +
 +
pg.escapeQuotesHTML(
 +
mw.message('group-' + groupName + '-member', user.gender).text()
 +
) +
 +
'</i>'
 +
);
 +
});
 +
}
 +
if (user.registration)
 +
ret.push(
 +
pg.escapeQuotesHTML(
 +
(user.editcount ? user.editcount : '0') +
 +
popupString(' edits since: ') +
 +
(user.registration ? formattedDate(new Date(user.registration)) : '')
 +
)
 +
);
 +
}
  
/**
+
if (queryobj.usercontribs && queryobj.usercontribs.length) {
  Sets the <code>innerHTML</code> attribute of the main div containing the popup content.
+
ret.push(
  @param {String} html The HTML to set.
+
popupString('last edit on ') + formattedDate(new Date(queryobj.usercontribs[0].timestamp))
*/
+
);
Navpopup.prototype.setInnerHTML = function (html) {
+
}
this.mainDiv.innerHTML = html;
 
};
 
  
/**
+
if (queryobj.blocks) {
  Updates the {@link #width} and {@link #height} attributes with the CSS properties.
+
ret.push(popupString('IP user')); //we only request list=blocks for IPs
  @private
+
for (var l = 0; l < queryobj.blocks.length; l++) {
*/
+
var rbstr =
Navpopup.prototype.updateDimensions = function () {
+
queryobj.blocks[l].rangestart === queryobj.blocks[l].rangeend ? 'BLOCK' : 'RANGEBLOCK';
this.width=parseInt(this.mainDiv.offsetWidth, 10);
+
rbstr = !Array.isArray(queryobj.blocks[l].restrictions)
this.height=parseInt(this.mainDiv.offsetHeight, 10);
+
? 'Has ' + rbstr.toLowerCase() + 's'
};
+
: rbstr + 'ED';
 +
ret.push('<b>' + popupString(rbstr) + '</b>');
 +
}
 +
}
  
/**
+
// if any element of ret ends with ' · ', merge it with the next element to avoid
  Checks if the point (x,y) is within {@link #fuzz} of the
+
// the .join(', ') call inserting a comma after it
  {@link #mainDiv}.
+
for (var m = 0; m < ret.length - 1; m++) {
  @param {integer} x x-coordinate (px)
+
if (ret[m].length > 3 && ret[m].substring(ret[m].length - 3) === ' · ') {
  @param {integer} y y-coordinate (px)
+
ret[m] = ret[m] + ret[m + 1];
  @type boolean
+
ret.splice(m + 1, 1); // delete element at index m+1
*/
+
m--;
Navpopup.prototype.isWithin = function(x,y) {
+
}
//~ If we're not even visible, no point should be considered as
+
}
//~ being within the popup.
 
if (!this.visible) { return false; }
 
this.updateDimensions();
 
var fuzz=this.fuzz || 0;
 
//~ Use a simple box metric here.
 
return (x+fuzz >= this.left && x-fuzz <= this.left + this.width &&
 
y+fuzz >= this.top  && y-fuzz <= this.top  + this.height);
 
};
 
  
/**
+
ret = '<hr />' + ret.join(', ');
  Adds a download to {@link #downloads}.
+
return ret;
  @param {Downloader} download
 
*/
 
Navpopup.prototype.addDownload=function(download) {
 
if (!download) { return; }
 
this.downloads.push(download);
 
};
 
/**
 
  Aborts the downloads listed in {@link #downloads}.
 
  @see Downloader#abort
 
*/
 
Navpopup.prototype.abortDownloads=function() {
 
for(var i=0; i<this.downloads.length; ++i) {
 
var d=this.downloads[i];
 
if (d && d.abort) { d.abort(); }
 
 
}
 
}
this.downloads=[];
 
};
 
  
 +
function APIcontribsPreviewHTML(article, download, navpop) {
 +
return APIhistoryPreviewHTML(article, download, navpop, true);
 +
}
  
/**
+
function APIhistoryPreviewHTML(article, download, navpop, reallyContribs) {
  A {@link Mousetracker} instance which is a property of the constructor (pseudo-global).
+
try {
*/
+
var jsobj = getJsObj(download.data);
Navpopup.tracker=new Mousetracker();
+
var edits = [];
// ENDFILE: navpopup.js
+
if (reallyContribs) {
// STARTFILE: diff.js
+
edits = jsobj.query.usercontribs;
//<NOLITE>
+
} else {
/*
+
edits = anyChild(jsobj.query.pages).revisions;
* Javascript Diff Algorithm
+
}
*  By John Resig (http://ejohn.org/) and [[:en:User:Lupin]]
 
*
 
* More Info:
 
*  http://ejohn.org/projects/javascript-diff-algorithm/
 
*/
 
 
 
function delFmt(x) {
 
if (!x.length) { return ''; }
 
return "<del class='popupDiff'>" + x.join('') +"</del>";
 
}
 
function insFmt(x) {
 
if (!x.length) { return ''; }
 
return "<ins class='popupDiff'>" + x.join('') +"</ins>";
 
}
 
  
function countCrossings(a, b, i, eject) {
+
var ret = editPreviewTable(article, edits, reallyContribs);
// count the crossings on the edge starting at b[i]
+
return ret;
if (!b[i].row && b[i].row !== 0) { return -1; }
+
} catch (someError) {
var count=0;
+
return 'History preview failed :-(';
for (var j=0; j<a.length; ++j) {
 
if (!a[j].row && a[j].row !== 0) { continue; }
 
if ( (j-b[i].row)*(i-a[j].row) > 0) {
 
if(eject) { return true; }
 
count++;
 
 
}
 
}
 
}
 
}
return count;
 
}
 
  
function shortenDiffString(str, context) {
+
//</NOLITE>
var re=RegExp('(<del[\\s\\S]*?</del>|<ins[\\s\\S]*?</ins>)');
+
// ENDFILE: querypreview.js
var splitted=str.parenSplit(re);
 
var ret=[''];
 
for (var i=0; i<splitted.length; i+=2) {
 
if (splitted[i].length < 2*context) {
 
ret[ret.length-1] += splitted[i];
 
if (i+1<splitted.length) { ret[ret.length-1] += splitted[i+1]; }
 
continue;
 
}
 
else {
 
if (i > 0) { ret[ret.length-1] += splitted[i].substring(0,context); }
 
if (i+1 < splitted.length) {
 
ret.push(splitted[i].substring(splitted[i].length-context) +
 
splitted[i+1]);
 
}
 
}
 
}
 
while (ret.length > 0 && !ret[0]) { ret = ret.slice(1); }
 
return ret;
 
}
 
  
 +
// STARTFILE: debug.js
 +
////////////////////////////////////////////////////////////////////
 +
// Debugging functions
 +
////////////////////////////////////////////////////////////////////
  
function diffString( o, n, simpleSplit ) {
+
function setupDebugging() {
var splitRe=RegExp('([[]{2}|[\\]]{2}|[{]{2,3}|[}]{2,3}|[|]|=|<|>|[*:]+|\\s|\\b)');
+
//<NOLITE>
 
+
if (window.popupDebug) {
//  We need to split the strings o and n first, and entify() the parts
+
// popupDebug is set from .version
// individually, so that the HTML entities are never cut apart. (AxelBoldt)
+
window.log = function (x) {
var out, i, oSplitted, nSplitted;
+
//if(gMsg!='')gMsg += '\n'; gMsg+=time() + ' ' + x; };
if (simpleSplit) {
+
window.console.log(x);
oSplitted=o.split(/\b/);  
+
};
nSplitted=n.split(/\b/);  
+
window.errlog = function (x) {
} else {
+
window.console.error(x);
oSplitted=o.parenSplit(splitRe);
+
};
nSplitted=n.parenSplit(splitRe);  
+
log('Initializing logger');
}
+
} else {
for (i=0; i<oSplitted.length; ++i) {oSplitted[i]=oSplitted[i].entify();}
+
//</NOLITE>
for (i=0; i<nSplitted.length; ++i) {nSplitted[i]=nSplitted[i].entify();}
+
window.log = function () {};
+
window.errlog = function () {};
out = diff (oSplitted, nSplitted);
+
//<NOLITE>
var str = "";
 
var acc=[]; // accumulator for prettier output
 
 
 
// crossing pairings -- eg 'A B' vs 'B A' -- cause problems, so let's iron them out
 
// this doesn't always do things optimally but it should be fast enough
 
var maxOutputPair=0;
 
for (i=0; i<out.n.length; ++i) {
 
if ( out.n[i].paired ) {
 
if( maxOutputPair > out.n[i].row ) {
 
// tangle - delete pairing
 
out.o[ out.n[i].row ]=out.o[ out.n[i].row ].text;
 
out.n[i]=out.n[i].text;
 
}
 
if (maxOutputPair < out.n[i].row) { maxOutputPair = out.n[i].row; }
 
 
}
 
}
 +
//</NOLITE>
 
}
 
}
 +
// ENDFILE: debug.js
  
// output the stuff preceding the first paired old line
+
// STARTFILE: images.js
for (i=0; i<out.o.length && !out.o[i].paired; ++i) { acc.push( out.o[i] ); }
 
str += delFmt(acc); acc=[];
 
  
// main loop
+
// load image of type Title.
for ( i = 0; i < out.n.length; ++i ) {
+
function loadImage(image, navpop) {
// output unpaired new "lines"
+
if (typeof image.stripNamespace != 'function') {
while ( i < out.n.length && !out.n[i].paired ) { acc.push( out.n[i++] ); }
+
alert('loadImages bad');
str += insFmt(acc); acc=[];
 
if ( i < out.n.length ) { // this new "line" is paired with the (out.n[i].row)th old "line"
 
str += out.n[i].text;
 
// output unpaired old rows starting after this new line's partner
 
var m = out.n[i].row + 1;
 
while ( m < out.o.length && !out.o[m].paired ) { acc.push ( out.o[m++] ); }
 
str += delFmt(acc); acc=[];
 
 
}
 
}
}
+
// API call to retrieve image info.
return str;
 
}
 
  
// see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object
+
if (!getValueOf('popupImages')) return;
// FIXME: use obj.hasOwnProperty instead of this kludge!
+
if (!isValidImageName(image)) return false;
var jsReservedProperties=RegExp('^(constructor|prototype|__((define|lookup)[GS]etter)__' +
 
  '|eval|hasOwnProperty|propertyIsEnumerable' +
 
  '|to(Source|String|LocaleString)|(un)?watch|valueOf)$');
 
function diffBugAlert(word) {
 
if (!diffBugAlert.list[word]) {
 
diffBugAlert.list[word]=1;
 
alert('Bad word: '+word+'\n\nPlease report this bug.');
 
}
 
}
 
diffBugAlert.list={};
 
  
function makeDiffHashtable(src) {
+
var art = image.urlString();
var ret={};
 
for ( var i = 0; i < src.length; i++ ) {
 
if ( jsReservedProperties.test(src[i]) ) { src[i] += '<!-- -->'; }
 
if ( !ret[ src[i] ] ) { ret[ src[i] ] = []; }
 
try { ret[ src[i] ].push( i ); } catch (err) { diffBugAlert(src[i]); }
 
}
 
return ret;
 
}
 
  
function diff( o, n ) {
+
var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query';
 +
url += '&prop=imageinfo&iiprop=url|mime&iiurlwidth=' + getValueOf('popupImageSizeLarge');
 +
url += '&titles=' + art;
  
// pass 1: make hashtable ns with new rows as keys
+
pendingNavpopTask(navpop);
var ns = makeDiffHashtable(n);
+
var callback = function (d) {
 
+
popupsInsertImage(navpop.idNumber, navpop, d);
// pass 2: make hashtable os with old rows as keys
+
};
var os = makeDiffHashtable(o);
+
var go = function () {
 
+
getPageWithCaching(url, callback, navpop);
// pass 3: pair unique new rows and matching unique old rows
+
return true;
var i;
+
};
for ( i in ns ) {
+
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
if ( ns[i].length == 1 && os[i] && os[i].length == 1 ) {
+
go();
n[ ns[i][0] ] = { text: n[ ns[i][0] ], row: os[i][0], paired: true };
+
} else {
o[ os[i][0] ] = { text: o[ os[i][0] ], row: ns[i][0], paired: true };
+
navpop.addHook(go, 'unhide', 'after', 'DOWNLOAD_IMAGE_QUERY_DATA');
 
}
 
}
 
}
 
}
  
// pass 4: pair matching rows immediately following paired rows (not necessarily unique)
+
function popupsInsertImage(id, navpop, download) {
for ( i = 0; i < n.length - 1; i++ ) {
+
log('popupsInsertImage');
if ( n[i].paired && ! n[i+1].paired && n[i].row + 1 < o.length && ! o[ n[i].row + 1 ].paired &&
+
var imageinfo;
n[i+1] == o[ n[i].row + 1 ] ) {
+
try {
n[i+1] = { text: n[i+1], row: n[i].row + 1, paired: true };
+
var jsObj = getJsObj(download.data);
o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1, paired: true };
+
var imagepage = anyChild(jsObj.query.pages);
 +
if (typeof imagepage.imageinfo === 'undefined') return;
 +
imageinfo = imagepage.imageinfo[0];
 +
} catch (someError) {
 +
log('popupsInsertImage failed :(');
 +
return;
 
}
 
}
}
 
  
// pass 5: pair matching rows immediately preceding paired rows (not necessarily unique)
+
var popupImage = document.getElementById('popupImg' + id);
for ( i = n.length - 1; i > 0; i-- ) {
+
if (!popupImage) {
if ( n[i].paired && ! n[i-1].paired && n[i].row > 0 && ! o[ n[i].row - 1 ].paired &&
+
log('could not find insertion point for image');
n[i-1] == o[ n[i].row - 1 ] ) {
+
return;
n[i-1] = { text: n[i-1], row: n[i].row - 1, paired: true };
 
o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1, paired: true };
 
 
}
 
}
}
 
  
return { o: o, n: n };
+
popupImage.width = getValueOf('popupImageSize');
}
+
popupImage.style.display = 'inline';
//</NOLITE>
 
// ENDFILE: diff.js
 
// STARTFILE: init.js
 
function setSiteInfo() {
 
if (window.popupLocalDebug) {
 
pg.wiki.hostname = 'en.wikipedia.org';
 
} else {
 
pg.wiki.hostname = location.hostname; // use in preference to location.hostname for flexibility (?)
 
}
 
pg.wiki.wikimedia=RegExp('(wiki([pm]edia|source|books|news|quote|versity)|wiktionary|mediawiki)[.]org').test(pg.wiki.hostname);
 
pg.wiki.wikia=RegExp('[.]wikia[.]com$', 'i').test(pg.wiki.hostname);
 
pg.wiki.isLocal=RegExp('^localhost').test(pg.wiki.hostname);
 
pg.wiki.commons=( pg.wiki.wikimedia && pg.wiki.hostname != 'commons.wikimedia.org') ? 'commons.wikimedia.org' : null;
 
pg.wiki.lang = mw.config.get('wgContentLanguage');
 
var port = location.port ? ':' + location.port : '';
 
pg.wiki.sitebase = pg.wiki.hostname + port;
 
}
 
  
function setTitleBase() {
+
// Set the source for the image.
var protocol = ( window.popupLocalDebug ? 'http:' : location.protocol );
+
if (imageinfo.thumburl) popupImage.src = imageinfo.thumburl;
pg.wiki.articlePath = mw.config.get('wgArticlePath').replace(/\/\$1/, "");  // as in http://some.thing.com/wiki/Article
+
else if (imageinfo.mime.indexOf('image') === 0) {
pg.wiki.botInterfacePath = mw.config.get('wgScript');
+
popupImage.src = imageinfo.url;
pg.wiki.APIPath = mw.config.get('wgScriptPath') +"/api.php";
+
log('a thumb could not be found, using original image');
// default mediawiki setting is paths like http://some.thing.com/articlePath/index.php?title=foo
+
} else log("fullsize imagethumb, but not sure if it's an image");
  
var titletail = pg.wiki.botInterfacePath + '?title=';
+
var a = document.getElementById('popupImageLink' + id);
//var titletail2 = joinPath([pg.wiki.botInterfacePath, 'wiki.phtml?title=']);
+
if (a === null) {
 +
return null;
 +
}
  
// other sites may need to add code here to set titletail depending on how their urls work
+
// Determine the action of the surrouding imagelink.
 +
switch (getValueOf('popupThumbAction')) {
 +
case 'imagepage':
 +
if (pg.current.article.namespaceId() != pg.nsImageId) {
 +
a.href = imageinfo.descriptionurl;
 +
// FIXME: unreliable pg.idNumber
 +
popTipsSoonFn('popupImage' + id)();
 +
break;
 +
}
 +
/* falls through */
 +
case 'sizetoggle':
 +
a.onclick = toggleSize;
 +
a.title = popupString('Toggle image size');
 +
return;
 +
case 'linkfull':
 +
a.href = imageinfo.url;
 +
a.title = popupString('Open full-size image');
 +
return;
 +
}
 +
}
  
pg.wiki.titlebase  = protocol + '//' + pg.wiki.sitebase + titletail;
+
// Toggles the image between inline small and navpop fullwidth.
//pg.wiki.titlebase2  = protocol + '//' + joinPath([pg.wiki.sitebase, titletail2]);
+
// It's the same image, no actual sizechange occurs, only display width.
pg.wiki.wikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.botInterfacePath;
+
function toggleSize() {
pg.wiki.apiwikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.APIPath;
+
var imgContainer = this;
pg.wiki.articlebase = protocol + '//' + pg.wiki.sitebase + pg.wiki.articlePath;
+
if (!imgContainer) {
pg.wiki.commonsbase = protocol + '//' + pg.wiki.commons  + pg.wiki.botInterfacePath;
+
alert('imgContainer is null :/');
pg.wiki.apicommonsbase = protocol + '//' + pg.wiki.commons  + pg.wiki.APIPath;
+
return;
pg.re.basenames = RegExp( '^(' +
+
}
  map( literalizeRegex, [ pg.wiki.titlebase, //pg.wiki.titlebase2,
+
var img = imgContainer.firstChild;
  pg.wiki.articlebase ]).join('|') + ')' );
+
if (!img) {
}
+
alert('img is null :/');
 +
return;
 +
}
  
 +
if (!img.style.width || img.style.width === '') {
 +
img.style.width = '100%';
 +
} else {
 +
img.style.width = '';
 +
}
 +
}
  
//////////////////////////////////////////////////
+
// Returns one title of an image from wikiText.
// Global regexps
+
function getValidImageFromWikiText(wikiText) {
 +
// nb in pg.re.image we're interested in the second bracketed expression
 +
// this may change if the regex changes :-(
 +
//var match=pg.re.image.exec(wikiText);
 +
var matched = null;
 +
var match;
 +
// strip html comments, used by evil bots :-(
 +
var t = removeMatchesUnless(
 +
wikiText,
 +
RegExp('(<!--[\\s\\S]*?-->)'),
 +
1,
 +
RegExp('^<!--[^[]*popup', 'i')
 +
);
  
function setMainRegex() {
+
while ((match = pg.re.image.exec(t))) {
var reStart='[^:]*://';
+
// now find a sane image name - exclude templates by seeking {
var preTitles = literalizeRegex( mw.config.get('wgScriptPath') ) + '/(?:index[.]php|wiki[.]phtml)[?]title=';
+
var m = match[2] || match[6];
preTitles += '|' + literalizeRegex( pg.wiki.articlePath + '/' );
+
if (isValidImageName(m)) {
 +
matched = m;
 +
break;
 +
}
 +
}
 +
pg.re.image.lastIndex = 0;
 +
if (!matched) {
 +
return null;
 +
}
 +
return mw.config.get('wgFormattedNamespaces')[pg.nsImageId] + ':' + upcaseFirst(matched);
 +
}
  
var reEnd='(' + preTitles + ')([^&?#]*)[^#]*(?:#(.+))?';
+
function removeMatchesUnless(str, re1, parencount, re2) {
pg.re.main = RegExp(reStart + literalizeRegex(pg.wiki.sitebase) + reEnd);
+
var split = str.parenSplit(re1);
}
+
var c = parencount + 1;
 +
for (var i = 0; i < split.length; ++i) {
 +
if (i % c === 0 || re2.test(split[i])) {
 +
continue;
 +
}
 +
split[i] = '';
 +
}
 +
return split.join('');
 +
}
  
function setRegexps() {
+
//</NOLITE>
setMainRegex();
+
// ENDFILE: images.js
var sp=nsRe(pg.nsSpecialId);
 
pg.re.urlNoPopup=RegExp('((title=|/)' + sp + '(?:%3A|:)|section=[0-9]|^#$)') ;
 
pg.re.contribs  =RegExp('(title=|/)'  + sp + '(?:%3A|:)Contributions' + '(&target=|/|/' + mw.config.get('wgFormattedNamespaces')[pg.nsUserId]+':)(.*)') ;
 
pg.re.email     =RegExp('(title=|/)'  + sp + '(?:%3A|:)EmailUser' + '(&target=|/|/(?:' + mw.config.get('wgFormattedNamespaces')[pg.nsUserId]+':)?)(.*)') ;
 
pg.re.backlinks =RegExp('(title=|/)'  + sp + '(?:%3A|:)WhatLinksHere' + '(&target=|/)([^&]*)');
 
pg.re.specialdiff=RegExp('/'          + sp + '(?:%3A|:)Diff/([^?#]*)');
 
  
//<NOLITE>
+
// STARTFILE: namespaces.js
var im=nsReImage();
+
// Set up namespaces and other non-strings.js localization
// note: tries to get images in infobox templates too, e.g. movie pages, album pages etc
+
// (currently that means redirs too)
//   (^|\[\[)image: *([^|\]]*[^|\] ]) *
 
//   (^|\[\[)image: *([^|\]]*[^|\] ])([^0-9\]]*([0-9]+) *px)?
 
// $4 = 120 as in 120px
 
pg.re.image = RegExp('(^|\\[\\[)' + im + ': *([^|\\]]*[^|\\] ])' +
 
'([^0-9\\]]*([0-9]+) *px)?|(?:\\n *[|]?|[|]) *' +
 
'(' + getValueOf('popupImageVarsRegexp') + ')' +
 
' *= *(?:\\[\\[ *)?(?:' + im + ':)?' +
 
'([^|]*?)(?:\\]\\])? *[|]? *\\n', 'img') ;
 
pg.re.imageBracketCount = 6;
 
  
pg.re.category = RegExp('\\[\\[' +nsRe(pg.nsCategoryId) +
+
function setNamespaces() {
': *([^|\\]]*[^|\\] ]) *', 'i');
+
pg.nsSpecialId = -1;
pg.re.categoryBracketCount = 1;
+
pg.nsMainspaceId = 0;
 +
pg.nsImageId = 6;
 +
pg.nsUserId = 2;
 +
pg.nsUsertalkId = 3;
 +
pg.nsCategoryId = 14;
 +
pg.nsTemplateId = 10;
 +
}
  
pg.re.ipUser=RegExp('^' +
+
function setRedirs() {
// IPv6
+
var r = 'redirect';
'(?::(?::|(?::[0-9A-Fa-f]{1,4}){1,7})|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}::|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){7})' +
+
var R = 'REDIRECT';
// IPv4
+
var redirLists = {
'|(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' +
+
//<NOLITE>
'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]))$');
+
ar: [R, 'تحويل'],
 +
be: [r, 'перанакіраваньне'],
 +
bg: [r, 'пренасочване', 'виж'],
 +
bs: [r, 'Preusmjeri', 'preusmjeri', 'PREUSMJERI'],
 +
bn: [R, 'পুনর্নির্দেশ'],
 +
cs: [R, 'PŘESMĚRUJ'],
 +
cy: [r, 'ail-cyfeirio'],
 +
de: [R, 'WEITERLEITUNG'],
 +
el: [R, 'ΑΝΑΚΑΤΕΥΘΥΝΣΗ'],
 +
eo: [R, 'ALIDIREKTU', 'ALIDIREKTI'],
 +
es: [R, 'REDIRECCIÓN'],
 +
et: [r, 'suuna'],
 +
ga: [r, 'athsheoladh'],
 +
gl: [r, 'REDIRECCIÓN', 'REDIRECIONAMENTO'],
 +
he: [R, 'הפניה'],
 +
hu: [R, 'ÁTIRÁNYÍTÁS'],
 +
is: [r, 'tilvísun', 'TILVÍSUN'],
 +
it: [R, 'RINVIA', 'Rinvia'],
 +
ja: [R, '転送'],
 +
mk: [r, 'пренасочување', 'види'],
 +
nds: [r, 'wiederleiden'],
 +
'nds-nl': [R, 'DEURVERWIEZING', 'DUURVERWIEZING'],
 +
nl: [R, 'DOORVERWIJZING'],
 +
nn: [r, 'omdiriger'],
 +
pl: [R, 'PATRZ', 'PRZEKIERUJ', 'TAM'],
 +
pt: [R, 'redir'],
 +
ru: [R, 'ПЕРЕНАПРАВЛЕНИЕ', 'ПЕРЕНАПР'],
 +
sk: [r, 'presmeruj'],
 +
sr: [r, 'Преусмери', 'преусмери', 'ПРЕУСМЕРИ', 'Preusmeri', 'preusmeri', 'PREUSMERI'],
 +
tt: [R, 'yünältü', 'перенаправление', 'перенапр'],
 +
uk: [R, 'ПЕРЕНАПРАВЛЕННЯ', 'ПЕРЕНАПР'],
 +
vi: [r, 'đổi'],
 +
zh: [R, '重定向'], // no comma
 +
//</NOLITE>
 +
};
 +
var redirList = redirLists[pg.wiki.lang] || [r, R];
 +
// Mediawiki is very tolerant about what comes after the #redirect at the start
 +
pg.re.redirect = RegExp(
 +
'^\\s*[#](' + redirList.join('|') + ').*?\\[{2}([^\\|\\]]*)(|[^\\]]*)?\\]{2}\\s*(.*)',
 +
'i'
 +
);
 +
}
  
pg.re.stub= RegExp(getValueOf('popupStubRegexp'), 'im');
+
function setInterwiki() {
pg.re.disambig=RegExp(getValueOf('popupDabRegexp'), 'im');
+
if (pg.wiki.wikimedia) {
 +
// From https://meta.wikimedia.org/wiki/List_of_Wikipedias
 +
//en.wikipedia.org/w/api.php?action=sitematrix&format=json&smtype=language&smlangprop=code&formatversion=2
 +
https: pg.wiki.interwiki =
 +
'aa|ab|ace|af|ak|als|am|an|ang|ar|arc|arz|as|ast|av|ay|az|ba|bar|bat-smg|bcl|be|be-x-old|bg|bh|bi|bjn|bm|bn|bo|bpy|br|bs|bug|bxr|ca|cbk-zam|cdo|ce|ceb|ch|cho|chr|chy|ckb|co|cr|crh|cs|csb|cu|cv|cy|da|de|diq|dsb|dv|dz|ee|el|eml|en|eo|es|et|eu|ext|fa|ff|fi|fiu-vro|fj|fo|fr|frp|frr|fur|fy|ga|gag|gan|gd|gl|glk|gn|got|gu|gv|ha|hak|haw|he|hi|hif|ho|hr|hsb|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|ilo|io|is|it|iu|ja|jbo|jv|ka|kaa|kab|kbd|kg|ki|kj|kk|kl|km|kn|ko|koi|kr|krc|ks|ksh|ku|kv|kw|ky|la|lad|lb|lbe|lg|li|lij|lmo|ln|lo|lt|ltg|lv|map-bms|mdf|mg|mh|mhr|mi|mk|ml|mn|mo|mr|mrj|ms|mt|mus|mwl|my|myv|mzn|na|nah|nap|nds|nds-nl|ne|new|ng|nl|nn|no|nov|nrm|nv|ny|oc|om|or|os|pa|pag|pam|pap|pcd|pdc|pfl|pi|pih|pl|pms|pnb|pnt|ps|pt|qu|rm|rmy|rn|ro|roa-rup|roa-tara|ru|rue|rw|sa|sah|sc|scn|sco|sd|se|sg|sh|si|simple|sk|sl|sm|sn|so|sq|sr|srn|ss|st|stq|su|sv|sw|szl|ta|te|tet|tg|th|ti|tk|tl|tn|to|tpi|tr|ts|tt|tum|tw|ty|udm|ug|uk|ur|uz|ve|vec|vi|vls|vo|wa|war|wo|wuu|xal|xh|yi|yo|za|zea|zh|zh-classical|zh-min-nan|zh-yue|zu';
 +
pg.re.interwiki = RegExp('^' + pg.wiki.interwiki + ':');
 +
} else {
 +
pg.wiki.interwiki = null;
 +
pg.re.interwiki = RegExp('^$');
 +
}
 +
}
  
//</NOLITE>
+
// return a regexp pattern matching all variants to write the given namespace
// FIXME replace with general parameter parsing function, this is daft
+
function nsRe(namespaceId) {
pg.re.oldid=RegExp('[?&]oldid=([^&]*)');
+
var imageNamespaceVariants = [];
pg.re.diff=RegExp('[?&]diff=([^&]*)');
+
jQuery.each(mw.config.get('wgNamespaceIds'), function (_localizedNamespaceLc, _namespaceId) {
}
+
if (_namespaceId != namespaceId) return;
 +
_localizedNamespaceLc = upcaseFirst(_localizedNamespaceLc);
 +
imageNamespaceVariants.push(
 +
mw.util.escapeRegExp(_localizedNamespaceLc).split(' ').join('[ _]')
 +
);
 +
imageNamespaceVariants.push(mw.util.escapeRegExp(encodeURI(_localizedNamespaceLc)));
 +
});
  
 +
return '(?:' + imageNamespaceVariants.join('|') + ')';
 +
}
  
//////////////////////////////////////////////////
+
function nsReImage() {
// miscellany
+
return nsRe(pg.nsImageId);
 +
}
 +
// ENDFILE: namespaces.js
  
function setupCache() {
+
// STARTFILE: selpop.js
// page caching
+
//<NOLITE>
pg.cache.pages = [];
+
function getEditboxSelection() {
}
+
// see http://www.webgurusforum.com/8/12/0
 +
var editbox;
 +
try {
 +
editbox = document.editform.wpTextbox1;
 +
} catch (dang) {
 +
return;
 +
}
 +
// IE, Opera
 +
if (document.selection) {
 +
return document.selection.createRange().text;
 +
}
 +
// Mozilla
 +
var selStart = editbox.selectionStart;
 +
var selEnd = editbox.selectionEnd;
 +
return editbox.value.substring(selStart, selEnd);
 +
}
  
function setMisc() {
+
function doSelectionPopup() {
pg.current.link=null;
+
// popup if the selection looks like [[foo|anything afterwards at all
pg.current.links=[];
+
// or [[foo|bar]]text without ']]'
pg.current.linksHash={};
+
// or [[foo|bar]]
 +
var sel = getEditboxSelection();
 +
var open = sel.indexOf('[[');
 +
var pipe = sel.indexOf('|');
 +
var close = sel.indexOf(']]');
 +
if (open == -1 || (pipe == -1 && close == -1)) {
 +
return;
 +
}
 +
if ((pipe != -1 && open > pipe) || (close != -1 && open > close)) {
 +
return;
 +
}
 +
var article = new Title(sel.substring(open + 2, pipe < 0 ? close : pipe));
 +
if (getValueOf('popupOnEditSelection') == 'boxpreview') {
 +
return doSeparateSelectionPopup(sel, article);
 +
}
 +
if (close > 0 && sel.substring(close + 2).indexOf('[[') >= 0) {
 +
return;
 +
}
 +
var a = document.createElement('a');
 +
a.href = pg.wiki.titlebase + article.urlString();
 +
mouseOverWikiLink2(a);
 +
if (a.navpopup) {
 +
a.navpopup.addHook(
 +
function () {
 +
runStopPopupTimer(a.navpopup);
 +
},
 +
'unhide',
 +
'after'
 +
);
 +
}
 +
}
  
setupCache();
+
function doSeparateSelectionPopup(str, article) {
 +
var div = document.getElementById('selectionPreview');
 +
if (!div) {
 +
div = document.createElement('div');
 +
div.id = 'selectionPreview';
 +
try {
 +
var box = document.editform.wpTextbox1;
 +
box.parentNode.insertBefore(div, box);
 +
} catch (error) {
 +
return;
 +
}
 +
}
 +
var p = prepPreviewmaker(str, article, newNavpopup(document.createElement('a'), article));
 +
p.makePreview();
 +
if (p.html) {
 +
div.innerHTML = p.html;
 +
}
 +
div.ranSetupTooltipsAlready = false;
 +
popTipsSoonFn('selectionPreview')();
 +
}
 +
//</NOLITE>
 +
// ENDFILE: selpop.js
  
pg.timer.checkPopupPosition=null;
+
// STARTFILE: navpopup.js
pg.counter.loop=0;
+
/**
 +
* @fileoverview  Defines two classes: {@link Navpopup} and {@link Mousetracker}.
 +
*
 +
* <code>Navpopup</code> describes popups: when they appear, where, what
 +
* they look like and so on.
 +
*
 +
* <code>Mousetracker</code> "captures" the mouse using
 +
* <code>document.onmousemove</code>.
 +
*/
  
// ids change with each popup: popupImage0, popupImage1 etc
+
/**
pg.idNumber=0;
+
* Creates a new Mousetracker.
 +
* @constructor
 +
* @class The Mousetracker class. This monitors mouse movements and manages associated hooks.
 +
*/
 +
function Mousetracker() {
 +
/**
 +
* Interval to regularly run the hooks anyway, in milliseconds.
 +
* @type Integer
 +
*/
 +
this.loopDelay = 400;
  
// for myDecodeURI
+
/**
pg.misc.decodeExtras = [
+
* Timer for the loop.
{from: '%2C', to: ',' },
+
* @type Timer
{from: '_',  to: ' ' },
+
*/
{from: '%24', to: '$'},
+
this.timer = null;
{from: '%26',  to: '&' } // no ,
 
];
 
  
}
+
/**
 +
* Flag - are we switched on?
 +
* @type Boolean
 +
*/
 +
this.active = false;
  
function leadingInteger(s){
+
/**
var n=s.match(/^(\d*)/)[1];
+
* Flag - are we probably inaccurate, i.e. not reflecting the actual mouse position?
if (n) { return +n; }
+
*/
return null;
+
this.dirty = true;
}
 
  
function setBrowserHacks() {
+
/**
var useOriginal=false;
+
* Array of hook functions.
var webkitUnder420 = false;
+
* @private
// browser-specific hacks
+
* @type Array
if (typeof window.opera != 'undefined') {
+
*/
//if (leadingInteger(opera.version()) < 9)
+
this.hooks = [];
{ useOriginal=true; } // v9 beta still seems to have buggy css
 
setDefault('popupNavLinkSeparator', ' &#183; ');
 
} else if (navigator.appName=='Konqueror') {
 
setDefault('popupNavLinkSeparator', ' &bull; ');
 
pg.flag.isKonq=true;
 
} else if ( navigator.vendor && navigator.vendor.toLowerCase().indexOf('apple computer')===0) {
 
pg.flag.isSafari=true;
 
var webkit=+navigator.userAgent.replace(RegExp('^.*AppleWebKit[/](\\d+).*', 'i'), '$1');
 
if (webkit < 420) {
 
useOriginal=true;
 
webkitUnder420 = true;
 
}
 
} else if (navigator.appName.indexOf("Microsoft")!=-1) {
 
setDefault('popupNavLinkSeparator', ' &#183; ');
 
var ver=+navigator.userAgent.replace(RegExp('^.*MSIE (\\d+).*'), '$1');
 
pg.flag.isIE=true;
 
pg.flag.IEVersion=ver;
 
 
}
 
}
if (pg.flag.isIE && pg.flag.IEVersion < 8) {
 
useOriginal=true;
 
}
 
if ((pg.flag.isIE && pg.flag.IEVersion < 7) || pg.flag.isKonq || (pg.flag.isSafari && webkitUnder420)) {
 
pg.flag.linksLikeIE6=true;
 
}
 
if (useOriginal && pg.structures.original) {
 
setDefault('popupStructure','original');
 
}
 
}
 
  
// We need a callback since this might end up asynchronous because of
+
/**
// the mw.loader.using() call.
+
* Adds a hook, to be called when we get events.
function setupPopups( callback ) {
+
* @param {Function} f A function which is called as
mw.loader.using( 'mediawiki.user', function() {
+
* <code>f(x,y)</code>. It should return <code>true</code> when it
// NB translatable strings should be set up first (strings.js)
+
* wants to be removed, and <code>false</code> otherwise.
// basics
+
*/
setupDebugging();
+
Mousetracker.prototype.addHook = function (f) {
setSiteInfo();
+
this.hooks.push(f);
setTitleBase();
+
};
setOptions(); // see options.js
 
  
// namespaces etc
+
/**
setNamespaces();
+
* Runs hooks, passing them the x
setInterwiki();
+
* and y coords of the mouse.  Hook functions that return true are
 +
* passed to {@link Mousetracker#removeHooks} for removal.
 +
* @private
 +
*/
 +
Mousetracker.prototype.runHooks = function () {
 +
if (!this.hooks || !this.hooks.length) {
 +
return;
 +
}
 +
//log('Mousetracker.runHooks; we got some hooks to run');
 +
var remove = false;
 +
var removeObj = {};
 +
// this method gets called a LOT -
 +
// pre-cache some variables
 +
var x = this.x,
 +
y = this.y,
 +
len = this.hooks.length;
  
// regexps
+
for (var i = 0; i < len; ++i) {
setRegexps();
+
//~ run the hook function, and remove it if it returns true
setRedirs();
+
if (this.hooks[i](x, y) === true) {
 +
remove = true;
 +
removeObj[i] = true;
 +
}
 +
}
 +
if (remove) {
 +
this.removeHooks(removeObj);
 +
}
 +
};
  
// other stuff
+
/**
setBrowserHacks();
+
* Removes hooks.
setMisc();
+
* @private
setupLivePreview();
+
* @param {Object} removeObj An object whose keys are the index
 
+
* numbers of functions for removal, with values that evaluate to true
// main deal here
+
*/
setupTooltips();
+
Mousetracker.prototype.removeHooks = function (removeObj) {
log('In setupPopups(), just called setupTooltips()');
+
var newHooks = [];
Navpopup.tracker.enable();
+
var len = this.hooks.length;
 
+
for (var i = 0; i < len; ++i) {
setupPopups.completed = true;
+
if (!removeObj[i]) {
if ( $.isFunction( callback ) ) {
+
newHooks.push(this.hooks[i]);
callback();
+
}
 
}
 
}
});
+
this.hooks = newHooks;
}
+
};
// ENDFILE: init.js
 
// STARTFILE: navlinks.js
 
//<NOLITE>
 
//////////////////////////////////////////////////
 
// navlinks... let the fun begin
 
//
 
  
function defaultNavlinkSpec() {
+
/**
var str='';
+
* Event handler for mouse wiggles.
str += '<b><<mainlink|shortcut= >></b>';
+
* We simply grab the event, set x and y and run the hooks.
if (getValueOf('popupLastEditLink')) {
+
* This makes the cpu all hot and bothered :-(
str += '*<<lastEdit|shortcut=/>>|<<lastContrib>>|<<sinceMe>>if(oldid){|<<oldEdit>>|<<diffCur>>}';
+
* @private
}
+
* @param {Event} e Mousemove event
 +
*/
 +
Mousetracker.prototype.track = function (e) {
 +
//~ Apparently this is needed in IE.
 +
e = e || window.event;
 +
var x, y;
 +
if (e) {
 +
if (e.pageX) {
 +
x = e.pageX;
 +
y = e.pageY;
 +
} else if (typeof e.clientX != 'undefined') {
 +
var left,
 +
top,
 +
docElt = document.documentElement;
  
// user links
+
if (docElt) {
// contribs - log - count - email - block
+
left = docElt.scrollLeft;
// count only if applicable; block only if popupAdminLinks
+
}
str += 'if(user){<br><<contribs|shortcut=c>>*<<userlog|shortcut=L|log>>';
+
left = left || document.body.scrollLeft || document.scrollLeft || 0;
str+='if(ipuser){*<<arin>>}if(wikimedia){*<<count|shortcut=#>>}';
 
str+='if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>|<<blocklog|log>>}}';
 
  
// editing links
+
if (docElt) {
// talkpage  -> edit|new - history - un|watch - article|edit
+
top = docElt.scrollTop;
// other page -> edit - history - un|watch - talk|edit|new
+
}
var editstr='<<edit|shortcut=e>>';
+
top = top || document.body.scrollTop || document.scrollTop || 0;
var editOldidStr='if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' + editstr + '}';
 
var historystr='<<history|shortcut=h>>if(mainspace_en){|<<editors|shortcut=E|>>}';
 
var watchstr='<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
 
  
str+='<br>if(talk){' +
+
x = e.clientX + left;
editOldidStr+'|<<new|shortcut=+>>' + '*' + historystr+'*'+watchstr + '*' +
+
y = e.clientY + top;
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
+
} else {
'}else{' + // not a talk page
+
return;
editOldidStr + '*' + historystr + '*' + watchstr + '*' +
+
}
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
+
this.setPosition(x, y);
 +
}
 +
};
  
// misc links
+
/**
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>*<<move|shortcut=m>>';
+
* Sets the x and y coordinates stored and takes appropriate action,
 
+
* running hooks as appropriate.
// admin links
+
* @param {Integer} x, y Screen coordinates to set
str += 'if(admin){<br><<unprotect|unprotectShort>>|<<protect|shortcut=p>>|<<protectlog|log>>*' +
+
*/
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>|<<deletelog|log>>}';
+
Mousetracker.prototype.setPosition = function (x, y) {
return str;
+
this.x = x;
}
+
this.y = y;
 +
if (this.dirty || this.hooks.length === 0) {
 +
this.dirty = false;
 +
return;
 +
}
 +
if (typeof this.lastHook_x != 'number') {
 +
this.lastHook_x = -100;
 +
this.lastHook_y = -100;
 +
}
 +
var diff = (this.lastHook_x - x) * (this.lastHook_y - y);
 +
diff = diff >= 0 ? diff : -diff;
 +
if (diff > 1) {
 +
this.lastHook_x = x;
 +
this.lastHook_y = y;
 +
if (this.dirty) {
 +
this.dirty = false;
 +
} else {
 +
this.runHooks();
 +
}
 +
}
 +
};
  
function navLinksHTML (article, hint, params) { //oldid, rcid) {
+
/**
var str = '<span class="popupNavLinks">' + defaultNavlinkSpec() + '</span>';
+
* Sets things in motion, unless they are already that is, registering an event handler on
// BAM
+
* <code>document.onmousemove</code>. A half-hearted attempt is made to preserve the old event
return navlinkStringToHTML(str, article, params);
+
* handler if there is one.
}
+
*/
 
+
Mousetracker.prototype.enable = function () {
function expandConditionalNavlinkString(s,article,z,recursionCount) {
+
if (this.active) {
var oldid=z.oldid, rcid=z.rcid, diff=z.diff;
+
return;
// nested conditionals (up to 10 deep) are ok, hopefully! (work from the inside out)
+
}
if (typeof recursionCount!=typeof 0) { recursionCount=0; }
+
this.active = true;
var conditionalSplitRegex=RegExp(
+
//~ Save the current handler for mousemove events. This isn't too
//(1 if \\( (2 2) \\)   {(3 3)}  (4  else   {(5 5)}  4)1)
+
//~ robust, of course.
'(;?\\s*if\\s*\\(\\s*([\\w]*)\\s*\\)\\s*\\{([^{}]*)\\}(\\s*else\\s*\\{([^{}]*?)\\}|))', 'i');
+
this.savedHandler = document.onmousemove;
var splitted=s.parenSplit(conditionalSplitRegex);
+
//~ Gotta save @tt{this} again for the closure, and use apply for
// $1: whole conditional
+
//~ the member function.
// $2: test condition
+
var savedThis = this;
// $3: true expansion
+
document.onmousemove = function (e) {
// $4: else clause (possibly empty)
+
savedThis.track.apply(savedThis, [e]);
// $5: false expansion (possibly null)
+
};
var numParens=5;
+
if (this.loopDelay) {
var ret = splitted[0];
+
this.timer = setInterval(function () {
for (var i=1; i<splitted.length; i=i+numParens+1) {
+
//log('loop delay in mousetracker is working');
 
+
savedThis.runHooks();
var testString=splitted[i+2-1];
+
}, this.loopDelay);
var trueString=splitted[i+3-1];
 
var falseString=splitted[i+5-1];
 
if (typeof falseString=='undefined' || !falseString) { falseString=''; }
 
var testResult=null;
 
 
 
switch (testString) {
 
case 'user':
 
testResult=(article.userName())?true:false;
 
break;
 
case 'talk':
 
testResult=(article.talkPage())?false:true; // talkPage converts _articles_ to talkPages
 
break;
 
case 'admin':
 
testResult=getValueOf('popupAdminLinks')?true:false;
 
break;
 
case 'oldid':
 
testResult=(typeof oldid != 'undefined' && oldid)?true:false;
 
break;
 
case 'rcid':
 
testResult=(typeof rcid != 'undefined' && rcid)?true:false;
 
break;
 
case 'ipuser':
 
testResult=(article.isIpUser())?true:false;
 
break;
 
case 'mainspace_en':
 
testResult=isInMainNamespace(article) &&
 
pg.wiki.hostname=='en.wikipedia.org';
 
break;
 
case 'wikimedia':
 
testResult=(pg.wiki.wikimedia) ? true : false;
 
break;
 
case 'diff':
 
testResult=(typeof diff != 'undefined' && diff)?true:false;
 
break;
 
 
}
 
}
 +
};
  
switch(testResult) {
+
/**
case null: ret+=splitted[i];  break;
+
* Disables the tracker, removing the event handler.
case true: ret+=trueString;  break;
+
*/
case false: ret+=falseString; break;
+
Mousetracker.prototype.disable = function () {
 +
if (!this.active) {
 +
return;
 
}
 
}
 
+
if (typeof this.savedHandler === 'function') {
// append non-conditional string
+
document.onmousemove = this.savedHandler;
ret += splitted[i+numParens];
+
} else {
}
+
delete document.onmousemove;
if (conditionalSplitRegex.test(ret) && recursionCount < 10) {
 
return expandConditionalNavlinkString(ret,article,z,recursionCount+1);
 
}
 
return ret;
 
}
 
 
 
function navlinkStringToArray(s, article, params) {
 
s=expandConditionalNavlinkString(s,article,params);
 
var splitted=s.parenSplit(RegExp('<<(.*?)>>'));
 
var ret=[];
 
for (var i=0; i<splitted.length; ++i) {
 
if (i%2) { // i odd, so s is a tag
 
var t=new navlinkTag();
 
var ss=splitted[i].split('|');
 
t.id=ss[0];
 
for (var j=1; j<ss.length; ++j) {
 
var sss=ss[j].split('=');
 
if (sss.length>1) {
 
t[sss[0]]=sss[1];
 
}
 
else { // no assignment (no "="), so treat this as a title (overwriting the last one)
 
t.text=popupString(sss[0]);
 
}
 
}
 
t.article=article;
 
var oldid=params.oldid, rcid=params.rcid, diff=params.diff;
 
if (typeof oldid !== 'undefined' && oldid !== null) { t.oldid=oldid; }
 
if (typeof rcid !== 'undefined' && rcid !== null) { t.rcid=rcid; }
 
if (typeof diff !== 'undefined' && diff !== null) { t.diff=diff; }
 
if (!t.text && t.id !== 'mainlink') { t.text=popupString(t.id); }
 
ret.push(t);
 
 
}
 
}
else { // plain HTML
+
if (this.timer) {
ret.push(splitted[i]);
+
clearInterval(this.timer);
 
}
 
}
}
+
this.active = false;
return ret;
+
};
}
+
 
 +
/**
 +
* Creates a new Navpopup.
 +
* Gets a UID for the popup and
 +
* @param init Contructor object. If <code>init.draggable</code> is true or absent, the popup becomes draggable.
 +
* @constructor
 +
* @class The Navpopup class. This generates popup hints, and does some management of them.
 +
*/
 +
function Navpopup(/*init*/) {
 +
//alert('new Navpopup(init)');
  
 +
/**
 +
* UID for each Navpopup instance.
 +
* Read-only.
 +
* @type integer
 +
*/
 +
this.uid = Navpopup.uid++;
  
function navlinkSubstituteHTML(s) {
+
/**
return s.split('*').join(getValueOf('popupNavLinkSeparator'))
+
* Read-only flag for current visibility of the popup.
.split('<menurow>').join('<li class="popup_menu_row">')
+
* @type boolean
.split('</menurow>').join('</li>')
+
* @private
.split('<menu>').join('<ul class="popup_menu">')
+
*/
.split('</menu>').join('</ul>');
+
this.visible = false;
  
}
+
/** Flag to be set when we want to cancel a previous request to
 +
* show the popup in a little while.
 +
* @private
 +
* @type boolean
 +
*/
 +
this.noshow = false;
  
function navlinkDepth(magic,s) {
+
/** Categorised list of hooks.
return s.split('<' + magic + '>').length - s.split('</' + magic + '>').length;
+
* @see #runHooks
}
+
* @see #addHook
 +
* @private
 +
* @type Object
 +
*/
 +
this.hooks = {
 +
create: [],
 +
unhide: [],
 +
hide: [],
 +
};
  
 +
/**
 +
* list of unique IDs of hook functions, to avoid duplicates
 +
* @private
 +
*/
 +
this.hookIds = {};
  
// navlinkString: * becomes the separator
+
/** List of downloads associated with the popup.
// <<foo|bar=baz|fubar>> becomes a foo-link with attribute bar='baz'
+
* @private
//   and visible text 'fubar'
+
* @type Array
// if(test){...} and if(test){...}else{...} work too (nested ok)
+
*/
 +
this.downloads = [];
  
function navlinkStringToHTML(s,article,params) {
+
/**
//limitAlert(navlinkStringToHTML, 5, 'navlinkStringToHTML\n' + article + '\n' + (typeof article));
+
* Number of uncompleted downloads.
var p=navlinkStringToArray(s,article,params);
+
* @type integer
var html='';
+
*/
var menudepth = 0; // nested menus not currently allowed, but doesn't do any harm to code for it
+
this.pending = null;
var menurowdepth = 0;
 
var wrapping = null;
 
for (var i=0; i<p.length; ++i) {
 
if (typeof p[i] == typeof '') {
 
html+=navlinkSubstituteHTML(p[i]);
 
menudepth += navlinkDepth('menu', p[i]);
 
menurowdepth += navlinkDepth('menurow', p[i]);
 
// if (menudepth === 0) {
 
// tagType='span';
 
// } else if (menurowdepth === 0) {
 
// tagType='li';
 
// } else {
 
// tagType = null;
 
// }
 
} else if (typeof p[i].type != 'undefined' && p[i].type=='navlinkTag') {
 
if (menudepth > 0 && menurowdepth === 0) {
 
html += '<li class="popup_menu_item">' + p[i].html() + '</li>';
 
} else {
 
html+=p[i].html();
 
}
 
}
 
}
 
return html;
 
}
 
  
function navlinkTag() {
+
/**
this.type='navlinkTag';
+
* Tolerance in pixels when detecting whether the mouse has left the popup.
}
+
* @type integer
 +
*/
 +
this.fuzz = 5;
  
navlinkTag.prototype.html=function () {
+
/**
this.getNewWin();
+
* Flag to toggle running {@link #limitHorizontalPosition} to regulate the popup's position.
this.getPrintFunction();
+
* @type boolean
var html='';
+
*/
var opening, closing;
+
this.constrained = true;
var tagType='span';
 
if (!tagType) {
 
opening = ''; closing = '';
 
} else {
 
opening = '<' + tagType + ' class="popup_' + this.id + '">';
 
closing = '</' + tagType + '>';
 
}
 
if (typeof this.print!='function') {
 
errlog ('Oh dear - invalid print function for a navlinkTag, id='+this.id);
 
} else {
 
html=this.print(this);
 
if (typeof html != typeof '') {html='';}
 
else if (typeof this.shortcut!='undefined') html=addPopupShortcut(html, this.shortcut);
 
}
 
return opening + html + closing;
 
};
 
  
navlinkTag.prototype.getNewWin=function() {
+
/**
getValueOf('popupLinksNewWindow');
+
* The popup width in pixels.
if (typeof pg.option.popupLinksNewWindow[this.id] === 'undefined') { this.newWin=null; }
+
* @private
this.newWin=pg.option.popupLinksNewWindow[this.id];
+
* @type integer
};
+
*/
 +
this.width = 0;
  
navlinkTag.prototype.getPrintFunction=function() { //think about this some more
+
/**
// this.id and this.article should already be defined
+
* The popup width in pixels.
if (typeof this.id!=typeof '' || typeof this.article!=typeof {} ) { return; }
+
* @private
var html='';
+
* @type integer
var a,t;
+
*/
 +
this.height = 0;
  
this.noPopup=1;
+
/**
switch (this.id) {
+
* The main content DIV element.
case 'contribs': case 'history': case 'whatLinksHere':
+
* @type HTMLDivElement
case 'userPage': case 'monobook': case 'userTalk':
+
*/
case 'talk': case 'article': case 'lastEdit':
+
this.mainDiv = null;
this.noPopup=null;
+
this.createMainDiv();
}
 
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) {
+
// if (!init || typeof init.popups_draggable=='undefined' || init.popups_draggable) {
case 'userTalk': case 'newUserTalk': case 'editUserTalk':
+
// this.makeDraggable(true);
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'); }
+
/**
 +
* A UID for each Navpopup. This constructor property is just a counter.
 +
* @type integer
 +
* @private
 +
*/
 +
Navpopup.uid = 0;
  
if (this.id != 'mainlink') {
+
/**
// FIXME anchor handling should be done differently with Title object
+
* Retrieves the {@link #visible} attribute, indicating whether the popup is currently visible.
this.article=this.article.removeAnchor();
+
* @type boolean
// if (typeof this.text=='undefined') this.text=popupString(this.id);
+
*/
}
+
Navpopup.prototype.isVisible = function () {
 +
return this.visible;
 +
};
  
switch (this.id) {
+
/**
case 'undelete':      this.print=specialLink; this.specialpage='Undelete'; this.sep='/'; break;
+
* Repositions popup using CSS style.
case 'whatLinksHere':  this.print=specialLink; this.specialpage='Whatlinkshere'; break;
+
* @private
case 'relatedChanges': this.print=specialLink; this.specialpage='Recentchangeslinked'; break;
+
* @param {integer} x x-coordinate (px)
case 'move':          this.print=specialLink; this.specialpage='Movepage'; break;
+
* @param {integer} y y-coordinate (px)
case 'contribs':      this.print=specialLink; this.specialpage='Contributions'; break;
+
* @param {boolean} noLimitHor Don't call {@link #limitHorizontalPosition}
case 'deletedContribs':this.print=specialLink; this.specialpage='Deletedcontributions'; break;
+
*/
case 'email':          this.print=specialLink; this.specialpage='EmailUser'; this.sep='/'; break;
+
Navpopup.prototype.reposition = function (x, y, noLimitHor) {
case 'block':          this.print=specialLink; this.specialpage='Blockip'; this.sep='&ip='; break;
+
log('reposition(' + x + ',' + y + ',' + noLimitHor + ')');
case 'unblock':        this.print=specialLink; this.specialpage='Ipblocklist'; this.sep='&action=unblock&ip='; break;
+
if (typeof x != 'undefined' && x !== null) {
case 'userlog':        this.print=specialLink; this.specialpage='Log'; this.sep='&user='; break;
+
this.left = x;
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; break;
 
case 'unwatch': case 'watch':
 
this.print=magicWatchLink; this.action=this.id+'&autowatchlist=1&autoimpl=' + popupString('autoedit_version') + '&actoken='+autoClickToken(); break;
 
case 'history': case 'historyfeed':
 
case 'unprotect': case 'protect':
 
this.print=wikiLink; this.action=this.id; break;
 
 
 
case 'delete':
 
this.print=wikiLink; this.action='delete';
 
if (this.article.namespaceId()==pg.nsImageId) {
 
var img=this.article.stripNamespace();
 
this.action+='&image='+img;
 
 
}
 
}
break;
+
if (typeof y != 'undefined' && y !== null) {
 
+
this.top = y;
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&ctype=text/css'; break;
 
case 'new':
 
this.print=wikiLink; this.action='edit&section=new'; break;
 
case 'mainlink':
 
if (typeof this.text=='undefined') { this.text=this.article.toString().entify(); }
 
if (getValueOf('popupSimplifyMainLink') && isInStrippableNamespace(this.article)) {
 
var s=this.text.split('/'); this.text=s[s.length-1];
 
if (this.text==='' && s.length > 1) { this.text=s[s.length-2]; }
 
 
}
 
}
this.print=titledWikiLink;
+
if (typeof this.left != 'undefined' && typeof this.top != 'undefined') {
if (typeof this.title==='undefined' && pg.current.link && typeof pg.current.link.href !== 'undefined') {
+
this.mainDiv.style.left = this.left + 'px';
this.title=safeDecodeURI((pg.current.link.originalTitle)?pg.current.link.originalTitle:this.article);
+
this.mainDiv.style.top = this.top + 'px';
if (typeof this.oldid !== 'undefined' && this.oldid) {
 
this.title=tprintf('Revision %s of %s', [this.oldid, this.title]);
 
}
 
 
}
 
}
this.action='view'; break;
+
if (!noLimitHor) {
case 'userPage':
+
this.limitHorizontalPosition();
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 'markpatrolled':
 
this.print=wikiLink; this.action='markpatrolled';
 
/* fall through */
 
case 'revert':
 
this.print=wikiLink; this.action='revert'; break;
 
case 'nullEdit':
 
this.print=wikiLink; this.action='nullEdit'; break;
 
case 'diffCur':
 
this.print=titledDiffLink;
 
this.title=tprintf('Show changes since revision %s', [this.oldid]);
 
this.from=this.oldid; this.to='cur'; break;
 
case 'editUserTalk':
 
case 'editTalk':
 
delete this.oldid;
 
this.article=this.article.talkPage();
 
this.action='edit'; this.print=wikiLink; break;
 
case 'newUserTalk':
 
case 'newTalk':
 
this.article=this.article.talkPage();
 
this.action='edit&section=new'; this.print=wikiLink; break;
 
case 'lastContrib':
 
case 'sinceMe':
 
this.print=magicHistoryLink;
 
break;
 
case 'togglePreviews':
 
this.text=popupString(pg.option.simplePopups ? 'enable previews' : 'disable previews');
 
/* fall through */
 
case 'disablePopups': case 'purgePopups':
 
this.print=popupMenuLink;
 
break;
 
default:
 
this.print=function () {return 'Unknown navlink type: '+this.id+'';};
 
}
 
};
 
//
 
//  end navlinks
 
//////////////////////////////////////////////////
 
//</NOLITE>
 
// ENDFILE: navlinks.js
 
// STARTFILE: shortcutkeys.js
 
//<NOLITE>
 
function popupHandleKeypress(evt) {
 
var keyCode = window.event ? window.event.keyCode : ( evt.keyCode ? evt.keyCode : evt.which);
 
if (!keyCode || !pg.current.link || !pg.current.link.navpopup) { return; }
 
if (keyCode==27) { // escape
 
killPopup();
 
return false; // swallow keypress
 
}
 
 
 
var letter=String.fromCharCode(keyCode);
 
var links=pg.current.link.navpopup.mainDiv.getElementsByTagName('A');
 
var startLink=0;
 
var i,j;
 
 
 
if (popupHandleKeypress.lastPopupLinkSelected) {
 
for (i=0; i<links.length; ++i) {
 
if (links[i]==popupHandleKeypress.lastPopupLinkSelected) { startLink=i; }
 
 
}
 
}
}
+
//console.log('navpop'+this.uid+' - (left,top)=(' + this.left + ',' + this.top + '), css=('
for (j=0; j<links.length; ++j) {
+
//+ this.mainDiv.style.left + ',' + this.mainDiv.style.top + ')');
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); }
+
* Prevents popups from being in silly locations. Hopefully.
return true;
+
* Should not be run if {@link #constrained} is true.
}
+
* @private
 
+
*/
function addPopupShortcuts() {
+
Navpopup.prototype.limitHorizontalPosition = function () {
if (document.onkeypress!=popupHandleKeypress) {
+
if (!this.constrained || this.tooWide) {
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;
 
return;
 
}
 
}
document.onkeypress=document.oldPopupOnkeypress;
+
this.updateDimensions();
} catch (nasties) { /* IE goes here */ }
+
var x = this.left;
}
+
var w = this.width;
 +
var cWidth = document.body.clientWidth;
  
 +
// log('limitHorizontalPosition: x='+x+
 +
// ', this.left=' + this.left +
 +
// ', this.width=' + this.width +
 +
// ', cWidth=' + cWidth);
  
function addLinkProperty(html, property) {
+
if (
// take "<a href=...>...</a> and add a property
+
x + w >= cWidth ||
// not sophisticated at all, easily broken
+
(x > 0 &&
var i=html.indexOf('>');
+
this.maxWidth &&
if (i<0) { return html; }
+
this.width < this.maxWidth &&
return html.substring(0,i) + ' ' + property + html.substring(i);
+
this.height > this.width &&
}
+
x > cWidth - this.maxWidth)
 
+
) {
function addPopupShortcut(html, key) {
+
// This is a very nasty hack. There has to be a better way!
if (!getValueOf('popupShortcutKeys')) { return html; }
+
// We find the "natural" width of the div by positioning it at the far left
var ret= addLinkProperty(html, 'popupkey="'+key+'"');
+
// then reset it so that it should be flush right (well, nearly)
if (key==' ') { key=popupString('spacebar'); }
+
this.mainDiv.style.left = '-10000px';
return ret.replace(RegExp('^(.*?)(title=")(.*?)(".*)$', 'i'),'$1$2$3 ['+key+']$4');
+
this.mainDiv.style.width = this.maxWidth + 'px';
}
+
var naturalWidth = parseInt(this.mainDiv.offsetWidth, 10);
//</NOLITE>
+
var newLeft = cWidth - naturalWidth - 1;
// ENDFILE: shortcutkeys.js
+
if (newLeft < 0) {
// STARTFILE: diffpreview.js
+
newLeft = 0;
//<NOLITE>
+
this.tooWide = true;
function loadDiff(article, oldid, diff, navpop) {
+
} // still unstable for really wide popups?
navpop.diffData={};
+
log(
var oldRev, newRev;
+
'limitHorizontalPosition: moving to (' +
switch (diff) {
+
newLeft +
case 'cur':
+
',' +
switch ( oldid ) {
+
this.top +
case null:
+
');' +
case '':
+
' naturalWidth=' +
case 'prev':
+
naturalWidth +
// eg newmessages diff link
+
', clientWidth=' +
oldRev='0&direction=prev';
+
cWidth
newRev=0;
+
);
break;
+
this.reposition(newLeft, null, true);
default:
 
oldRev = oldid;
 
newRev = ( oldid || 0 ) + '&direction=cur';
 
 
}
 
}
break;
+
};
case 'prev':
 
oldRev = ( oldid || 0 ) + '&direction=prev'; newRev = oldid; break;
 
case 'next':
 
oldRev = oldid; newRev = oldid + '&direction=next';
 
break;
 
default:
 
oldRev = oldid || 0; newRev = diff || 0; break;
 
}
 
oldRev = oldRev || 0;
 
newRev = newRev || 0;
 
  
var go = function() {
+
/**
pendingNavpopTask(navpop);
+
* Counter indicating the z-order of the "highest" popup.
getWiki(article, doneDiffNew, newRev, navpop);
+
* We start the z-index at 1000 so that popups are above everything
 +
* else on the screen.
 +
* @private
 +
* @type integer
 +
*/
 +
Navpopup.highest = 1000;
  
pendingNavpopTask(navpop);
+
/**
getWiki(article, doneDiffOld, oldRev, navpop);
+
* Brings popup to the top of the z-order.
 +
* We increment the {@link #highest} property of the contructor here.
 +
* @private
 +
*/
 +
Navpopup.prototype.raise = function () {
 +
this.mainDiv.style.zIndex = Navpopup.highest + 1;
 +
++Navpopup.highest;
 +
};
  
var tz = Cookie.read('popTz');
+
/**
if ( mw.config.get('wgEnableAPI') && getValueOf('popupAdjustDiffDates') && tz === null) {
+
* Shows the popup provided {@link #noshow} is not true.
pendingNavpopTask(navpop);
+
* Updates the position, brings the popup to the top of the z-order and unhides it.
getPageWithCaching(pg.wiki.apiwikibase + '?format=json&action=query&meta=userinfo&uiprop=options',
+
*/
  function(d) {
+
Navpopup.prototype.show = function () {
  completedNavpopTask(navpop);
+
//document.title+='s';
  setTimecorrectionCookie(d);
+
if (this.noshow) {
  if (diffDownloadsComplete(navpop)) { insertDiff(navpop); }
+
return;
  },  navpop);
 
 
}
 
}
return true; // remove hook once run
+
//document.title+='t';
 +
this.reposition();
 +
this.raise();
 +
this.unhide();
 
};
 
};
if (navpop.visible || !getValueOf('popupLazyDownloads')) { go(); }
 
else { navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_DIFFS'); }
 
}
 
  
function setTimecorrectionCookie(d) {
+
/**
try {
+
* Checks to see if the mouse pointer has
var jsobj=getJsObj(d.data);
+
* stabilised (checking every <code>time</code>/2 milliseconds) and runs the
var tz=jsobj.query.userinfo.options.timecorrection;
+
* {@link #show} method if it has.
Cookie.create( 'popTz', getTimeOffset(tz), 1);
+
* @param {integer} time The minimum time (ms) before the popup may be shown.
} catch (someError) {
+
*/
logerr( 'setTimecorretion failed' );
+
Navpopup.prototype.showSoonIfStable = function (time) {
return;
+
log('showSoonIfStable, time=' + time);
}
+
if (this.visible) {
}
+
return;
 
 
function doneDiff(download, isOld) {
 
if (!download.owner || !download.owner.diffData) { return; }
 
var navpop=download.owner;
 
var label= (isOld) ? 'Old' : 'New';
 
var otherLabel=(isOld) ? 'New' : 'Old';
 
navpop.diffData[label]=download;
 
completedNavpopTask(download.owner);
 
if (diffDownloadsComplete(navpop)) { insertDiff(navpop); }
 
}
 
 
 
function diffDownloadsComplete(navpop) {
 
if ( Cookie.read('popTz')===null) { return false; }
 
return navpop.diffData.Old && navpop.diffData.New;
 
}
 
 
 
function doneDiffNew(download) { doneDiff(download, false); }
 
function doneDiffOld(download) { doneDiff(download, true);  }
 
 
 
function rmBoringLines(a,b,context) {
 
 
 
if (typeof context == 'undefined') { context=2; }
 
// this is fairly slow... i think it's quicker than doing a word-based diff from the off, though
 
var aa=[], aaa=[];
 
var bb=[], bbb=[];
 
var i, j;
 
 
 
// first, gather all disconnected nodes in a and all crossing nodes in a and b
 
for (i=0; i<a.length; ++i ) {
 
if(!a[i].paired) { aa[i]=1; }
 
else if (countCrossings(b,a,i, true)) {
 
aa[i]=1;
 
bb[ a[i].row ] = 1;
 
 
}
 
}
}
+
this.noshow = false;
  
// pick up remaining disconnected nodes in b
+
//~ initialize these variables so that we never run @tt{show} after
for (i=0; i<b.length; ++i ) {
+
//~ just half the time
if (bb[i]==1) { continue; }
+
this.stable_x = -10000;
if(!b[i].paired) { bb[i]=1; }
+
this.stable_y = -10000;
}
 
  
// another pass to gather context: we want the neighbours of included nodes which are not yet included
+
var stableShow = function () {
// we have to add in partners of these nodes, but we don't want to add context for *those* nodes in the next pass
+
log('stableShow called');
for (i=0; i<b.length; ++i) {
+
var new_x = Navpopup.tracker.x,
if ( bb[i] == 1 ) {
+
new_y = Navpopup.tracker.y;
for (j=max(0,i-context); j < min(b.length, i+context); ++j) {
+
var dx = savedThis.stable_x - new_x,
if ( !bb[j] ) { bb[j] = 1; aa[ b[j].row ] = 0.5; }
+
dy = savedThis.stable_y - new_y;
 +
var fuzz2 = 0; // savedThis.fuzz * savedThis.fuzz;
 +
//document.title += '[' + [savedThis.stable_x,new_x, savedThis.stable_y,new_y, dx, dy, fuzz2].join(',') + '] ';
 +
if (dx * dx <= fuzz2 && dy * dy <= fuzz2) {
 +
log('mouse is stable');
 +
clearInterval(savedThis.showSoonStableTimer);
 +
savedThis.reposition.apply(savedThis, [new_x + 2, new_y + 2]);
 +
savedThis.show.apply(savedThis, []);
 +
savedThis.limitHorizontalPosition.apply(savedThis, []);
 +
return;
 
}
 
}
 +
savedThis.stable_x = new_x;
 +
savedThis.stable_y = new_y;
 +
};
 +
var savedThis = this;
 +
this.showSoonStableTimer = setInterval(stableShow, time / 2);
 +
};
 +
 +
/**
 +
* Sets the {@link #noshow} flag and hides the popup. This should be called
 +
* when the mouse leaves the link before
 +
* (or after) it's actually been displayed.
 +
*/
 +
Navpopup.prototype.banish = function () {
 +
log('banish called');
 +
// hide and prevent showing with showSoon in the future
 +
this.noshow = true;
 +
if (this.showSoonStableTimer) {
 +
log('clearing showSoonStableTimer');
 +
clearInterval(this.showSoonStableTimer);
 
}
 
}
}
+
this.hide();
 +
};
  
for (i=0; i<a.length; ++i) {
+
/**
if ( aa[i] == 1 ) {
+
* Runs hooks added with {@link #addHook}.
for (j=max(0,i-context); j < min(a.length, i+context); ++j) {
+
* @private
if ( !aa[j] ) { aa[j] = 1; bb[ a[j].row ] = 0.5; }
+
* @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
 +
* @param {String} when Controls exactly when the hook is run: either 'before' or 'after'
 +
*/
 +
Navpopup.prototype.runHooks = function (key, when) {
 +
if (!this.hooks[key]) {
 +
return;
 +
}
 +
var keyHooks = this.hooks[key];
 +
var len = keyHooks.length;
 +
for (var i = 0; i < len; ++i) {
 +
if (keyHooks[i] && keyHooks[i].when == when) {
 +
if (keyHooks[i].hook.apply(this, [])) {
 +
// remove the hook
 +
if (keyHooks[i].hookId) {
 +
delete this.hookIds[keyHooks[i].hookId];
 +
}
 +
keyHooks[i] = null;
 +
}
 
}
 
}
 
}
 
}
}
+
};
  
for (i=0; i<bb.length; ++i) {
+
/**
if (bb[i] > 0) { // it's a row we need
+
* Adds a hook to the popup. Hook functions are run with <code>this</code> set to refer to the
if (b[i].paired) { bbb.push(b[i].text); } // joined; partner should be in aa
+
* Navpopup instance, and no arguments.
else {
+
* @param {Function} hook The hook function. Functions that return true are deleted.
bbb.push(b[i]);
+
* @param {String} key Key name of the {@link #hooks} array - one of 'create', 'unhide', 'hide'
}
+
* @param {String} when Controls exactly when the hook is run: either 'before' or 'after'
 +
* @param {String} uid A truthy string identifying the hook function; if it matches another hook
 +
* in this position, it won't be added again.
 +
*/
 +
Navpopup.prototype.addHook = function (hook, key, when, uid) {
 +
when = when || 'after';
 +
if (!this.hooks[key]) {
 +
return;
 
}
 
}
}
+
// if uid is specified, don't add duplicates
for (i=0; i<aa.length; ++i) {
+
var hookId = null;
if (aa[i] > 0) { // it's a row we need
+
if (uid) {
if (a[i].paired) { aaa.push(a[i].text); } // joined; partner should be in aa
+
hookId = [key, when, uid].join('|');
else {
+
if (this.hookIds[hookId]) {
aaa.push(a[i]);
+
return;
 
}
 
}
 +
this.hookIds[hookId] = true;
 
}
 
}
}
+
this.hooks[key].push({ hook: hook, when: when, hookId: hookId });
 +
};
  
return { a: aaa, b: bbb};
+
/**
}
+
* Creates the main DIV element, which contains all the actual popup content.
 +
* Runs hooks with key 'create'.
 +
* @private
 +
*/
 +
Navpopup.prototype.createMainDiv = function () {
 +
if (this.mainDiv) {
 +
return;
 +
}
 +
this.runHooks('create', 'before');
 +
var mainDiv = document.createElement('div');
  
function stripOuterCommonLines(a,b,context) {
+
var savedThis = this;
var i=0;
+
mainDiv.onclick = function (e) {
while (i<a.length && i < b.length && a[i]==b[i]) { ++i; }
+
savedThis.onclickHandler(e);
var j=a.length-1; var k=b.length-1;
+
};
while ( j>=0 && k>=0 && a[j]==b[k] ) { --j; --k; }
+
mainDiv.className = this.className ? this.className : 'navpopup_maindiv';
 +
mainDiv.id = mainDiv.className + this.uid;
  
return { a: a.slice(max(0,i - 1 - context), min(a.length+1, j + context+1)),
+
mainDiv.style.position = 'absolute';
b: b.slice(max(0,i - 1 - context), min(b.length+1, k + context+1)) };
+
mainDiv.style.minWidth = '350px';
}
+
mainDiv.style.display = 'none';
 +
mainDiv.className = 'navpopup';
  
function insertDiff(navpop) {
+
// easy access to javascript object through DOM functions
// for speed reasons, we first do a line-based diff, discard stuff that seems boring, then do a word-based diff
+
mainDiv.navpopup = this;
// FIXME: sometimes this gives misleading diffs as distant chunks are squashed together
 
var oldlines=navpop.diffData.Old.data.split('\n');
 
var newlines=navpop.diffData.New.data.split('\n');
 
var inner=stripOuterCommonLines(oldlines,newlines,getValueOf('popupDiffContextLines'));
 
oldlines=inner.a; newlines=inner.b;
 
var truncated=false;
 
getValueOf('popupDiffMaxLines');
 
if (oldlines.length > pg.option.popupDiffMaxLines || newlines.length > pg.option.popupDiffMaxLines) {
 
// truncate
 
truncated=true;
 
inner=stripOuterCommonLines(oldlines.slice(0,pg.option.popupDiffMaxLines),
 
newlines.slice(0,pg.option.popupDiffMaxLines),
 
pg.option.popupDiffContextLines);
 
oldlines=inner.a; newlines=inner.b;
 
}
 
  
var lineDiff=diff(oldlines, newlines);
+
this.mainDiv = mainDiv;
var lines2=rmBoringLines(lineDiff.o, lineDiff.n);
+
document.body.appendChild(mainDiv);
var oldlines2=lines2.a; var newlines2=lines2.b;
+
this.runHooks('create', 'after');
 +
};
  
var simpleSplit = !String.prototype.parenSplit.isNative;
+
/**
var html='<hr />';
+
* Calls the {@link #raise} method.
if (getValueOf('popupDiffDates')) {
+
* @private
html += diffDatesTable(navpop.diffData.Old, navpop.diffData.New);
+
*/
html += '<hr />';
+
Navpopup.prototype.onclickHandler = function (/*e*/) {
}
+
this.raise();
html += shortenDiffString(
+
};
diffString(oldlines2.join('\n'), newlines2.join('\n'), simpleSplit),
 
getValueOf('popupDiffContextCharacters') ).join('<hr />');
 
setPopupTipsAndHTML(html.split('\n').join('<br>') +
 
(truncated ? '<hr /><b>'+popupString('Diff truncated for performance reasons')+'</b>' : '') ,
 
'popupPreview', navpop.idNumber);
 
}
 
  
function diffDatesTable( oldDl, newDl ) {
+
/**
var html='<table class="popup_diff_dates">';
+
* Makes the popup draggable, using a {@link Drag} object.
html += diffDatesTableRow( newDl, tprintf('New revision'));
+
* @private
html += diffDatesTableRow( oldDl, tprintf('Old revision'));
+
*/
html += '</table>';
+
Navpopup.prototype.makeDraggable = function (handleName) {
return html;
+
if (!this.mainDiv) {
}
+
this.createMainDiv();
function diffDatesTableRow( dl, label ) {
+
}
var txt='';
+
var drag = new Drag();
if (!dl) {
+
if (!handleName) {
txt=popupString('Something went wrong :-(');
+
drag.startCondition = function (e) {
} else if (!dl.lastModified) {
+
try {
txt= (/^\s*$/.test(dl.data)) ?
+
if (!e.shiftKey) {
popupString('Empty revision, maybe non-existent') : popupString('Unknown date');
+
return false;
} else {
+
}
var datePrint=getValueOf('popupDiffDatePrinter');
+
} catch (err) {
if (typeof dl.lastModified[datePrint] == 'function') {
+
return false;
if (getValueOf('popupAdjustDiffDates')) {
 
var off = Cookie.read('popTz');
 
if (off) {
 
var d2 = adjustDate(dl.lastModified, off);
 
txt = dayFormat(d2, true) + ' ' + timeFormat(d2, true);
 
 
}
 
}
} else {
+
return true;
txt = dl.lastModified[datePrint]();
+
};
}
 
} else {
 
txt = tprintf('Invalid %s %s', ['popupDiffDatePrinter', datePrint]);
 
 
}
 
}
}
+
var dragHandle;
var revlink = generalLink({url: dl.url.replace(/&.*?(oldid=[0-9]+(?:&direction=[^&]*)?).*/, '&$1'),
+
if (handleName) dragHandle = document.getElementById(handleName);
  text: label, title: label});
+
if (!dragHandle) dragHandle = this.mainDiv;
return simplePrintf('<tr><td>%s</td><td>%s</td></tr>', [ revlink, txt ]);
+
var np = this;
}
+
drag.endHook = function (x, y) {
//</NOLITE>
+
Navpopup.tracker.dirty = true;
// ENDFILE: diffpreview.js
+
np.reposition(x, y);
// STARTFILE: links.js
+
};
//<NOLITE>
+
drag.init(dragHandle, this.mainDiv);
/////////////////////
+
};
// LINK GENERATION //
 
/////////////////////
 
  
// titledDiffLink --> titledWikiLink --> generalLink
+
/**
// wikiLink   --> titledWikiLink --> generalLink
+
* Hides the popup using CSS. Runs hooks with key 'hide'.
// editCounterLink --> generalLink
+
* Sets {@link #visible} appropriately.
 +
* {@link #banish} should be called externally instead of this method.
 +
* @private
 +
*/
 +
Navpopup.prototype.hide = function () {
 +
this.runHooks('hide', 'before');
 +
this.abortDownloads();
 +
if (typeof this.visible != 'undefined' && this.visible) {
 +
this.mainDiv.style.display = 'none';
 +
this.visible = false;
 +
}
 +
this.runHooks('hide', 'after');
 +
};
  
// TODO Make these functions return Element objects, not just raw HTML strings.
+
/**
 +
* Shows the popup using CSS. Runs hooks with key 'unhide'.
 +
* Sets {@link #visible} appropriately.  {@link #show} should be called externally instead of this method.
 +
* @private
 +
*/
 +
Navpopup.prototype.unhide = function () {
 +
this.runHooks('unhide', 'before');
 +
if (typeof this.visible != 'undefined' && !this.visible) {
 +
this.mainDiv.style.display = 'inline';
 +
this.visible = true;
 +
}
 +
this.runHooks('unhide', 'after');
 +
};
  
function titledDiffLink(l) { // article, text, title, from, to) {
+
/**
return titledWikiLink({article: l.article, action: l.to + '&oldid=' + l.from,
+
* Sets the <code>innerHTML</code> attribute of the main div containing the popup content.
newWin: l.newWin,
+
* @param {String} html The HTML to set.
noPopup: l.noPopup,
+
*/
text: l.text, title: l.title,
+
Navpopup.prototype.setInnerHTML = function (html) {
/* hack: no oldid here */
+
this.mainDiv.innerHTML = html;
actionName: 'diff'});
+
};
}
 
  
 +
/**
 +
* Updates the {@link #width} and {@link #height} attributes with the CSS properties.
 +
* @private
 +
*/
 +
Navpopup.prototype.updateDimensions = function () {
 +
this.width = parseInt(this.mainDiv.offsetWidth, 10);
 +
this.height = parseInt(this.mainDiv.offsetHeight, 10);
 +
};
  
function wikiLink(l) {
+
/**
//{article:article, action:action, text:text, oldid, newid}) {
+
* Checks if the point (x,y) is within {@link #fuzz} of the
if (! (typeof l.article == typeof {} &&
+
* {@link #mainDiv}.
typeof l.action == typeof '' &&
+
* @param {integer} x x-coordinate (px)
typeof l.text==typeof '')) return null;
+
* @param {integer} y y-coordinate (px)
if (typeof l.oldid == 'undefined') { l.oldid=null; }
+
* @type boolean
var savedOldid = l.oldid;
+
*/
if (!/^(edit|view|revert|render)$|^raw/.test(l.action)) { l.oldid=null; }
+
Navpopup.prototype.isWithin = function (x, y) {
var hint=popupString(l.action + 'Hint'); // revertHint etc etc etc
+
//~ If we're not even visible, no point should be considered as
var oldidData=[l.oldid, safeDecodeURI(l.article)];
+
//~ being within the popup.
var revisionString = tprintf('revision %s of %s', oldidData);
+
if (!this.visible) {
log('revisionString='+revisionString);
+
return false;
switch (l.action) {
 
case 'edit&section=new': hint = popupString('newSectionHint');  break;
 
case 'edit&undo=':
 
if (l.diff && l.diff != 'prev' && savedOldid ) {
 
  l.action += l.diff + '&undoafter=' + savedOldid;
 
} else if (savedOldid) {
 
  l.action += savedOldid;
 
 
}
 
}
hint = popupString('undoHint');
+
this.updateDimensions();
break;
+
var fuzz = this.fuzz || 0;
case 'raw&ctype=text/css': hint=popupString('rawHint'); break;
+
//~ Use a simple box metric here.
case 'revert':
+
return (
if ( !mw.config.get('wgEnableAPI') ) {
+
x + fuzz >= this.left &&
alert( 'This function of navigation popups now requires a MediaWiki ' +
+
x - fuzz <= this.left + this.width &&
'installation with the API enabled.');
+
y + fuzz >= this.top &&
break;
+
y - fuzz <= this.top + this.height
 +
);
 +
};
 +
 
 +
/**
 +
* Adds a download to {@link #downloads}.
 +
* @param {Downloader} download
 +
*/
 +
Navpopup.prototype.addDownload = function (download) {
 +
if (!download) {
 +
return;
 
}
 
}
var p=parseParams(pg.current.link.href);
+
this.downloads.push(download);
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) {
+
* Aborts the downloads listed in {@link #downloads}.
hint = simplePrintf(hint, [revisionString]);
+
* @see Downloader#abort
 +
*/
 +
Navpopup.prototype.abortDownloads = function () {
 +
for (var i = 0; i < this.downloads.length; ++i) {
 +
var d = this.downloads[i];
 +
if (d && d.abort) {
 +
d.abort();
 +
}
 
}
 
}
else {
+
this.downloads = [];
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});
+
* A {@link Mousetracker} instance which is a property of the constructor (pseudo-global).
}
+
*/
 +
Navpopup.tracker = new Mousetracker();
 +
// ENDFILE: navpopup.js
  
function revertSummary(oldid, diff) {
+
// STARTFILE: diff.js
var ret='';
+
//<NOLITE>
if (diff == 'prev') {
+
/*
ret=getValueOf('popupQueriedRevertToPreviousSummary');
+
* Javascript Diff Algorithm
} else { ret = getValueOf('popupQueriedRevertSummary'); }
+
*  By John Resig (http://ejohn.org/) and [[:en:User:Lupin]]
return ret + '&autorv=' + oldid;
+
*
}
+
* More Info:
 +
*  http://ejohn.org/projects/javascript-diff-algorithm/
 +
*/
  
function titledWikiLink(l) {
+
function delFmt(x) {
// possible properties of argument:
+
if (!x.length) {
// article, action, text, title, oldid, actionName, className, noPopup
+
return '';
// oldid = null is fine here
+
}
 +
return "<del class='popupDiff'>" + x.join('') + '</del>';
 +
}
  
// article and action are mandatory args
+
function insFmt(x) {
 +
if (!x.length) {
 +
return '';
 +
}
 +
return "<ins class='popupDiff'>" + x.join('') + '</ins>';
 +
}
  
if (typeof l.article == 'undefined' || typeof l.action=='undefined') {
+
function countCrossings(a, b, i, eject) {
errlog('got undefined article or action in titledWikiLink');
+
// count the crossings on the edge starting at b[i]
return null;
+
if (!b[i].row && b[i].row !== 0) {
 +
return -1;
 +
}
 +
var count = 0;
 +
for (var j = 0; j < a.length; ++j) {
 +
if (!a[j].row && a[j].row !== 0) {
 +
continue;
 +
}
 +
if ((j - b[i].row) * (i - a[j].row) > 0) {
 +
if (eject) {
 +
return true;
 +
}
 +
count++;
 +
}
 +
}
 +
return count;
 
}
 
}
  
var base = pg.wiki.titlebase + l.article.urlString();
+
function shortenDiffString(str, context) {
var url=base;
+
var re = RegExp('(<del[\\s\\S]*?</del>|<ins[\\s\\S]*?</ins>)');
 +
var splitted = str.parenSplit(re);
 +
var ret = [''];
 +
for (var i = 0; i < splitted.length; i += 2) {
 +
if (splitted[i].length < 2 * context) {
 +
ret[ret.length - 1] += splitted[i];
 +
if (i + 1 < splitted.length) {
 +
ret[ret.length - 1] += splitted[i + 1];
 +
}
 +
continue;
 +
} else {
 +
if (i > 0) {
 +
ret[ret.length - 1] += splitted[i].substring(0, context);
 +
}
 +
if (i + 1 < splitted.length) {
 +
ret.push(splitted[i].substring(splitted[i].length - context) + splitted[i + 1]);
 +
}
 +
}
 +
}
 +
while (ret.length > 0 && !ret[0]) {
 +
ret = ret.slice(1);
 +
}
 +
return ret;
 +
}
  
if (typeof l.actionName=='undefined' || !l.actionName) { l.actionName='action'; }
+
function diffString(o, n, simpleSplit) {
 +
var splitRe = RegExp('([[]{2}|[\\]]{2}|[{]{2,3}|[}]{2,3}|[|]|=|<|>|[*:]+|\\s|\\b)');
  
// no need to add &action=view, and this confuses anchors
+
// We need to split the strings o and n first, and entify() the parts
if (l.action != 'view') { url = base + '&' + l.actionName + '=' + l.action; }
+
//  individually, so that the HTML entities are never cut apart. (AxelBoldt)
 +
var out, i, oSplitted, nSplitted;
 +
if (simpleSplit) {
 +
oSplitted = o.split(/\b/);
 +
nSplitted = n.split(/\b/);
 +
} else {
 +
oSplitted = o.parenSplit(splitRe);
 +
nSplitted = n.parenSplit(splitRe);
 +
}
 +
for (i = 0; i < oSplitted.length; ++i) {
 +
oSplitted[i] = oSplitted[i].entify();
 +
}
 +
for (i = 0; i < nSplitted.length; ++i) {
 +
nSplitted[i] = nSplitted[i].entify();
 +
}
  
if (typeof l.oldid!='undefined' && l.oldid) { url+='&oldid='+l.oldid; }
+
out = diff(oSplitted, nSplitted);
 +
var str = '';
 +
var acc = []; // accumulator for prettier output
  
var cssClass=pg.misc.defaultNavlinkClassname;
+
// crossing pairings -- eg 'A B' vs 'B A' -- cause problems, so let's iron them out
if (typeof l.className!='undefined' && l.className) { cssClass=l.className; }
+
// this doesn't always do things optimally but it should be fast enough
 +
var maxOutputPair = 0;
 +
for (i = 0; i < out.n.length; ++i) {
 +
if (out.n[i].paired) {
 +
if (maxOutputPair > out.n[i].row) {
 +
// tangle - delete pairing
 +
out.o[out.n[i].row] = out.o[out.n[i].row].text;
 +
out.n[i] = out.n[i].text;
 +
}
 +
if (maxOutputPair < out.n[i].row) {
 +
maxOutputPair = out.n[i].row;
 +
}
 +
}
 +
}
  
return generalNavLink({url: url, newWin: l.newWin,
+
// output the stuff preceding the first paired old line
title: (typeof l.title != 'undefined') ? l.title : null,
+
for (i = 0; i < out.o.length && !out.o[i].paired; ++i) {
text: (typeof l.text!='undefined')?l.text:null,
+
acc.push(out.o[i]);
className: cssClass, noPopup:l.noPopup, onclick:l.onclick});
+
}
}
+
str += delFmt(acc);
 +
acc = [];
  
pg.fn.getLastContrib = function getLastContrib(wikipage, newWin) {
+
// main loop
getHistoryInfo(wikipage, function(x) {
+
for (i = 0; i < out.n.length; ++i) {
processLastContribInfo(x, {page: wikipage, newWin: newWin});
+
// output unpaired new "lines"
});
+
while (i < out.n.length && !out.n[i].paired) {
};
+
acc.push(out.n[i++]);
 
+
}
function processLastContribInfo(info, stuff) {
+
str += insFmt(acc);
if(!info.edits || !info.edits.length) { alert('Popups: an odd thing happened. Please retry.'); return; }
+
acc = [];
if(!info.firstNewEditor) {
+
if (i < out.n.length) {
alert(tprintf('Only found one editor: %s made %s edits', [info.edits[0].editor,info.edits.length]));
+
// this new "line" is paired with the (out.n[i].row)th old "line"
return;
+
str += out.n[i].text;
 +
// output unpaired old rows starting after this new line's partner
 +
var m = out.n[i].row + 1;
 +
while (m < out.o.length && !out.o[m].paired) {
 +
acc.push(out.o[m++]);
 +
}
 +
str += delFmt(acc);
 +
acc = [];
 +
}
 +
}
 +
return str;
 
}
 
}
var newUrl=pg.wiki.titlebase + new Title(stuff.page).urlString() + '&diff=cur&oldid='+info.firstNewEditor.oldid;
 
displayUrl(newUrl, stuff.newWin);
 
}
 
  
pg.fn.getDiffSinceMyEdit = function getDiffSinceMyEdit(wikipage, newWin) {
+
// see http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object
getHistoryInfo(wikipage, function(x){
+
// FIXME: use obj.hasOwnProperty instead of this kludge!
processDiffSinceMyEdit(x, {page: wikipage, newWin: newWin});
+
var jsReservedProperties = RegExp(
});
+
'^(constructor|prototype|__((define|lookup)[GS]etter)__' +
};
+
'|eval|hasOwnProperty|propertyIsEnumerable' +
 +
'|to(Source|String|LocaleString)|(un)?watch|valueOf)$'
 +
);
  
function processDiffSinceMyEdit(info, stuff) {
+
function diffBugAlert(word) {
if(!info.edits || !info.edits.length) { alert('Popups: something fishy happened. Please try again.'); return; }
+
if (!diffBugAlert.list[word]) {
var friendlyName=stuff.page.split('_').join(' ');
+
diffBugAlert.list[word] = 1;
if(!info.myLastEdit) {
+
alert('Bad word: ' + word + '\n\nPlease report this bug.');
alert(tprintf('Couldn\'t find an edit by %s\nin the last %s edits to\n%s',
+
}
  [info.userName, getValueOf('popupHistoryLimit'), friendlyName]));
 
return;
 
}
 
if(info.myLastEdit.index === 0) {
 
alert(tprintf("%s seems to be the last editor to the page %s", [info.userName, friendlyName]));
 
return;
 
 
}
 
}
var newUrl=pg.wiki.titlebase + new Title(stuff.page).urlString() + '&diff=cur&oldid='+ info.myLastEdit.oldid;
 
displayUrl(newUrl, stuff.newWin);
 
}
 
  
function displayUrl(url, newWin){
+
diffBugAlert.list = {};
if(newWin) { window.open(url); }
 
else { document.location=url; }
 
}
 
  
pg.fn.purgePopups = function purgePopups() {
+
function makeDiffHashtable(src) {
processAllPopups(true);
+
var ret = {};
setupCache(); // deletes all cached items (not browser cached, though...)
+
for (var i = 0; i < src.length; i++) {
pg.option={};
+
if (jsReservedProperties.test(src[i])) {
abortAllDownloads();
+
src[i] += '<!-- -->';
};
+
}
 
+
if (!ret[src[i]]) {
function processAllPopups(nullify, banish) {
+
ret[src[i]] = [];
for (var i=0; pg.current.links && i<pg.current.links.length; ++i) {
+
}
if (!pg.current.links[i].navpopup) { continue; }
+
try {
if (nullify || banish) pg.current.links[i].navpopup.banish();
+
ret[src[i]].push(i);
pg.current.links[i].simpleNoMore=false;
+
} catch (err) {
if (nullify) pg.current.links[i].navpopup=null;
+
diffBugAlert(src[i]);
 +
}
 +
}
 +
return ret;
 
}
 
}
}
 
  
pg.fn.disablePopups = function disablePopups(){
+
function diff(o, n) {
processAllPopups(false, true);
+
// pass 1: make hashtable ns with new rows as keys
setupTooltips(null, true);
+
var ns = makeDiffHashtable(n);
};
 
  
pg.fn.togglePreviews = function togglePreviews() {
+
// pass 2: make hashtable os with old rows as keys
processAllPopups(true, true);
+
var os = makeDiffHashtable(o);
pg.option.simplePopups=!pg.option.simplePopups;
 
abortAllDownloads();
 
};
 
  
function magicWatchLink(l) {
+
// pass 3: pair unique new rows and matching unique old rows
//Yuck!! Would require a thorough redesign to add this as a click event though ...
+
var i;
l.onclick = simplePrintf( 'pg.fn.modifyWatchlist(\'%s\',\'%s\');return false;', [l.article.toString(true).split("\\").join("\\\\").split("'").join("\\'"), this.id] );
+
for (i in ns) {
return wikiLink(l);
+
if (ns[i].length == 1 && os[i] && os[i].length == 1) {
}
+
n[ns[i][0]] = { text: n[ns[i][0]], row: os[i][0], paired: true };
 +
o[os[i][0]] = { text: o[os[i][0]], row: ns[i][0], paired: true };
 +
}
 +
}
  
pg.fn.modifyWatchlist = function modifyWatchlist(title, action) {
+
// pass 4: pair matching rows immediately following paired rows (not necessarily unique)
var reqData = {
+
for (i = 0; i < n.length - 1; i++) {
'action': 'watch',
+
if (
'format': 'json',
+
n[i].paired &&
'title': title,
+
!n[i + 1].paired &&
'token': mw.user.tokens.get('watchToken'),
+
n[i].row + 1 < o.length &&
'uselang': mw.config.get('wgUserLanguage')
+
!o[n[i].row + 1].paired &&
};
+
n[i + 1] == o[n[i].row + 1]
if (action==='unwatch') reqData.unwatch = '';
+
) {
 +
n[i + 1] = { text: n[i + 1], row: n[i].row + 1, paired: true };
 +
o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1, paired: true };
 +
}
 +
}
  
jQuery.ajax({
+
// pass 5: pair matching rows immediately preceding paired rows (not necessarily unique)
url: mw.util.wikiScript('api'),
+
for (i = n.length - 1; i > 0; i--) {
dataType: 'json',
+
if (
type: 'POST',
+
n[i].paired &&
data: reqData,
+
!n[i - 1].paired &&
success: function( data, textStatus, xhr ) {
+
n[i].row > 0 &&
mw.util.jsMessage( data.watch.message, 'watch' );
+
!o[n[i].row - 1].paired &&
 +
n[i - 1] == o[n[i].row - 1]
 +
) {
 +
n[i - 1] = { text: n[i - 1], row: n[i].row - 1, paired: true };
 +
o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1, paired: true };
 
}
 
}
});
+
}
};
 
  
function magicHistoryLink(l) {
+
return { o: o, n: n };
// FIXME use onclick change href trick to sort this out instead of window.open
 
 
 
var jsUrl='', title='', onClick='';
 
switch(l.id) {
 
case 'lastContrib':
 
onClick=simplePrintf('pg.fn.getLastContrib(\'%s\',%s)',
 
[l.article.toString(true).split("\\").join("\\\\").split("'").join("\\'"), l.newWin]);
 
title=popupString('lastContribHint');
 
break;
 
case 'sinceMe':
 
onClick=simplePrintf('pg.fn.getDiffSinceMyEdit(\'%s\',%s)',
 
[l.article.toString(true).split("\\").join("\\\\").split("'").join("\\'"), l.newWin]);
 
title=popupString('sinceMeHint');
 
break;
 
 
}
 
}
jsUrl = 'javascript:' + onClick; // jshint ignore:line
+
//</NOLITE>
onClick += ';return false;';
+
// ENDFILE: diff.js
  
return generalNavLink({url: jsUrl, newWin: false, // can't have new windows with JS links, I think
+
// STARTFILE: init.js
title: title, text: l.text, noPopup: l.noPopup, onclick: onClick });
+
function setSiteInfo() {
}
+
if (window.popupLocalDebug) {
 
+
pg.wiki.hostname = 'en.wikipedia.org';
function popupMenuLink(l) {
+
} else {
var jsUrl=simplePrintf('javascript:pg.fn.%s()', [l.id]); // jshint ignore:line
+
pg.wiki.hostname = location.hostname; // use in preference to location.hostname for flexibility (?)
var title=popupString(simplePrintf('%sHint', [l.id]));
 
var onClick=simplePrintf('pg.fn.%s();return false;', [l.id]);
 
return generalNavLink({url: jsUrl, newWin:false, title:title, text:l.text, noPopup:l.noPopup, onclick: onClick});
 
}
 
 
 
function specialLink(l) {
 
// properties: article, specialpage, text, sep
 
if (typeof l.specialpage=='undefined'||!l.specialpage) return null;
 
var base = pg.wiki.titlebase +  mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId]+':'+l.specialpage;
 
if (typeof l.sep == 'undefined' || l.sep === null) l.sep='&target=';
 
var article=l.article.urlString({keepSpaces: l.specialpage=='Search'});
 
var hint=popupString(l.specialpage+'Hint');
 
switch (l.specialpage) {
 
case 'Log':
 
switch (l.sep) {
 
case '&user=': hint=popupString('userLogHint'); break;
 
case '&type=block&page=': hint=popupString('blockLogHint'); break;
 
case '&page=': hint=popupString('pageLogHint'); break;
 
case '&type=protect&page=': hint=popupString('protectLogHint'); break;
 
case '&type=delete&page=': hint=popupString('deleteLogHint'); break;
 
default: log('Unknown log type, sep=' + l.sep); hint='Missing hint (FIXME)';
 
 
}
 
}
break;
+
pg.wiki.wikimedia = RegExp(
case 'PrefixIndex': article += '/'; break;
+
'(wiki([pm]edia|source|books|news|quote|versity|species|voyage|data)|metawiki|wiktionary|mediawiki)[.]org'
 +
).test(pg.wiki.hostname);
 +
pg.wiki.wikia = RegExp('[.]wikia[.]com$', 'i').test(pg.wiki.hostname);
 +
pg.wiki.isLocal = RegExp('^localhost').test(pg.wiki.hostname);
 +
pg.wiki.commons =
 +
pg.wiki.wikimedia && pg.wiki.hostname != 'commons.wikimedia.org'
 +
? 'commons.wikimedia.org'
 +
: null;
 +
pg.wiki.lang = mw.config.get('wgContentLanguage');
 +
var port = location.port ? ':' + location.port : '';
 +
pg.wiki.sitebase = pg.wiki.hostname + port;
 
}
 
}
if (hint) hint = simplePrintf(hint, [safeDecodeURI(l.article)]);
 
else hint = safeDecodeURI(l.specialpage+':'+l.article) ;
 
  
var url = base + l.sep + article;
+
function setUserInfo() {
return generalNavLink({url: url, title: hint, text: l.text, newWin:l.newWin, noPopup:l.noPopup});
+
var params = {
}
+
action: 'query',
 +
list: 'users',
 +
ususers: mw.config.get('wgUserName'),
 +
usprop: 'rights',
 +
};
 +
 
 +
pg.user.canReview = false;
 +
if (getValueOf('popupReview')) {
 +
getMwApi()
 +
.get(params)
 +
.done(function (data) {
 +
var rights = data.query.users[0].rights;
 +
pg.user.canReview = rights.indexOf('review') !== -1; // TODO: Should it be a getValueOf('ReviewRight') ?
 +
});
 +
}
 +
}
  
function generalLink(l) {
+
function fetchSpecialPageNames() {
// l.url, l.text, l.title, l.newWin, l.className, l.noPopup, l.onclick
+
var params = {
if (typeof l.url=='undefined') return null;
+
action: 'query',
 +
meta: 'siteinfo',
 +
siprop: 'specialpagealiases',
 +
formatversion: 2,
 +
// cache for an hour
 +
uselang: 'content',
 +
maxage: 3600,
 +
};
 +
return getMwApi()
 +
.get(params)
 +
.then(function (data) {
 +
pg.wiki.specialpagealiases = data.query.specialpagealiases;
 +
});
 +
}
  
// only quotation marks in the url can screw us up now... I think
+
function setTitleBase() {
var url=l.url.split('"').join('%22');
+
var protocol = window.popupLocalDebug ? 'http:' : location.protocol;
 +
pg.wiki.articlePath = mw.config.get('wgArticlePath').replace(/\/\$1/, ''); // as in http://some.thing.com/wiki/Article
 +
pg.wiki.botInterfacePath = mw.config.get('wgScript');
 +
pg.wiki.APIPath = mw.config.get('wgScriptPath') + '/api.php';
 +
// default mediawiki setting is paths like http://some.thing.com/articlePath/index.php?title=foo
  
var ret='<a href="' + url + '"';
+
var titletail = pg.wiki.botInterfacePath + '?title=';
if (typeof l.title!='undefined' && l.title) { ret += ' title="' + pg.escapeQuotesHTML(l.title) + '"'; }
+
//var titletail2 = joinPath([pg.wiki.botInterfacePath, 'wiki.phtml?title=']);
if (typeof l.onclick!='undefined' && l.onclick) { ret += ' onclick="' + pg.escapeQuotesHTML(l.onclick) + '"'; }
 
if (l.noPopup) { ret += ' noPopup=1'; }
 
var newWin;
 
if (typeof l.newWin=='undefined' || l.newWin === null) { newWin=getValueOf('popupNewWindows'); }
 
else { newWin=l.newWin; }
 
if (newWin) { ret += ' target="_blank"'; }
 
if (typeof l.className!='undefined'&&l.className) { ret+=' class="'+l.className+'"'; }
 
ret += '>';
 
if (typeof l.text==typeof '') { ret+= l.text; }
 
ret +='</a>';
 
return ret;
 
}
 
  
function appendParamsToLink(linkstr, params) {
+
// other sites may need to add code here to set titletail depending on how their urls work
var sp=linkstr.parenSplit(RegExp('(href="[^"]+?)"', 'i'));
 
if (sp.length<2) return null;
 
var ret=sp.shift() + sp.shift();
 
ret += '&' + params + '"';
 
ret += sp.join('');
 
return ret;
 
}
 
  
function changeLinkTargetLink(x) { // newTarget, text, hint, summary, clickButton, minor, title (optional) {
+
pg.wiki.titlebase = protocol + '//' + pg.wiki.sitebase + titletail;
if (x.newTarget) {
+
//pg.wiki.titlebase2  = protocol + '//' + joinPath([pg.wiki.sitebase, titletail2]);
log ('changeLinkTargetLink: newTarget=' + x.newTarget);
+
pg.wiki.wikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.botInterfacePath;
}
+
pg.wiki.apiwikibase = protocol + '//' + pg.wiki.sitebase + pg.wiki.APIPath;
if (x.oldTarget !== decodeURIComponent( x.oldTarget ) ) {
+
pg.wiki.articlebase = protocol + '//' + pg.wiki.sitebase + pg.wiki.articlePath;
log ('This might be an input problem: ' + x.oldTarget );
+
pg.wiki.commonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.botInterfacePath;
 +
pg.wiki.apicommonsbase = protocol + '//' + pg.wiki.commons + pg.wiki.APIPath;
 +
pg.re.basenames = RegExp(
 +
'^(' +
 +
map(literalizeRegex, [
 +
pg.wiki.titlebase, //pg.wiki.titlebase2,
 +
pg.wiki.articlebase,
 +
]).join('|') +
 +
')'
 +
);
 
}
 
}
  
// FIXME: first character of page title as well as namespace should be case insensitive
+
//////////////////////////////////////////////////
// eg [category:foo] and [Category:Foo] are equivalent
+
// Global regexps
// this'll break if charAt(0) is nasty
 
var cA=literalizeRegex(x.oldTarget);
 
var chs=cA.charAt(0).toUpperCase();
 
chs='['+chs + chs.toLowerCase()+']';
 
var currentArticleRegexBit=chs+cA.substring(1);
 
currentArticleRegexBit=currentArticleRegexBit
 
.split(RegExp('(?:[_ ]+|%20)', 'g')).join('(?:[_ ]+|%20)')
 
.split('\\(').join('(?:%28|\\()')
 
.split('\\)').join('(?:%29|\\))'); // why does this need to match encoded strings ? links in the document ?
 
// leading and trailing space should be ignored, and anchor bits optional:
 
currentArticleRegexBit = '\\s*(' + currentArticleRegexBit + '(?:#[^\\[\\|]*)?)\\s*';
 
// e.g. Computer (archaic) -> \s*([Cc]omputer[_ ](?:%2528|\()archaic(?:%2528|\)))\s*
 
  
// autoedit=s~\[\[([Cc]ad)\]\]~[[Computer-aided%20design|$1]]~g;s~\[\[([Cc]AD)[|]~[[Computer-aided%20design|~g
+
function setMainRegex() {
 +
var reStart = '[^:]*://';
 +
var preTitles =
 +
literalizeRegex(mw.config.get('wgScriptPath')) + '/(?:index[.]php|wiki[.]phtml)[?]title=';
 +
preTitles += '|' + literalizeRegex(pg.wiki.articlePath + '/');
  
var title=x.title || mw.config.get('wgPageName').split('_').join(' ');
+
var reEnd = '(' + preTitles + ')([^&?#]*)[^#]*(?:#(.+))?';
var lk=titledWikiLink({article: new Title(title), newWin:x.newWin,
+
pg.re.main = RegExp(reStart + literalizeRegex(pg.wiki.sitebase) + reEnd);
action:  'edit',
 
text: x.text,
 
title:  x.hint,
 
className: 'popup_change_title_link'
 
});
 
var cmd='';
 
if (x.newTarget) {
 
// escape '&' and other nasties
 
var t=x.newTarget;
 
var s=literalizeRegex(x.newTarget);
 
cmd += 's~\\[\\['+currentArticleRegexBit+'\\]\\]~[['+t+'|$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 buildSpecialPageGroup(specialPageObj) {
 +
var variants = [];
 +
variants.push(mw.util.escapeRegExp(specialPageObj['realname']));
 +
variants.push(mw.util.escapeRegExp(encodeURI(specialPageObj['realname'])));
 +
specialPageObj.aliases.forEach(function (alias) {
 +
variants.push(mw.util.escapeRegExp(alias));
 +
variants.push(mw.util.escapeRegExp(encodeURI(alias)));
 +
});
 +
return variants.join('|');
 +
}
  
function redirLink(redirMatch, article) {
+
function setRegexps() {
// NB redirMatch is in wikiText
+
setMainRegex();
var ret='';
+
var sp = nsRe(pg.nsSpecialId);
 +
pg.re.urlNoPopup = RegExp('((title=|/)' + sp + '(?:%3A|:)|section=[0-9]|^#$)');
  
if (getValueOf('popupAppendRedirNavLinks') && getValueOf('popupNavLinks')) {
+
pg.wiki.specialpagealiases.forEach(function (specialpage) {
ret += '<hr />';
+
if (specialpage.realname === 'Contributions') {
if (getValueOf('popupFixRedirs') && typeof autoEdit != 'undefined' && autoEdit) {
+
pg.re.contribs = RegExp(
log('redirLink: newTarget=' + redirMatch);
+
'(title=|/)' +
ret += addPopupShortcut(changeLinkTargetLink({
+
sp +
newTarget: redirMatch,
+
'(?:%3A|:)(?:' +
text: popupString('Redirects'),
+
buildSpecialPageGroup(specialpage) +
hint: popupString('Fix this redirect'),
+
')' +
summary: simplePrintf(getValueOf('popupFixRedirsSummary'),[article.toString(), redirMatch]),
+
'(&target=|/|/' +
oldTarget: article.toString(),
+
nsRe(pg.nsUserId) +
clickButton: getValueOf('popupRedirAutoClick'),
+
':)(.*)',
minor: true,
+
'i'
watch: getValueOf('popupWatchRedirredPages')
+
);
}), 'R');
+
} else if (specialpage.realname === 'Diff') {
ret += popupString(' to ');
+
pg.re.specialdiff = RegExp(
}
+
'/' + sp + '(?:%3A|:)(?:' + buildSpecialPageGroup(specialpage) + ')' + '/([^?#]*)',
else ret += popupString('Redirects') + popupString(' to ');
+
'i'
return ret;
+
);
}
+
} else if (specialpage.realname === 'Emailuser') {
 +
pg.re.email = RegExp(
 +
'(title=|/)' +
 +
sp +
 +
'(?:%3A|:)(?:' +
 +
buildSpecialPageGroup(specialpage) +
 +
')' +
 +
'(&target=|/|/(?:' +
 +
nsRe(pg.nsUserId) +
 +
':)?)(.*)',
 +
'i'
 +
);
 +
} else if (specialpage.realname === 'Whatlinkshere') {
 +
pg.re.backlinks = RegExp(
 +
'(title=|/)' +
 +
sp +
 +
'(?:%3A|:)(?:' +
 +
buildSpecialPageGroup(specialpage) +
 +
')' +
 +
'(&target=|/)([^&]*)',
 +
'i'
 +
);
 +
}
 +
});
  
else return '<br> ' + popupString('Redirects') + popupString(' to ') +
+
//<NOLITE>
titledWikiLink({article: new Title().fromWikiText(redirMatch), action: 'view',  /* FIXME: newWin */
+
var im = nsReImage();
  text: safeDecodeURI(redirMatch), title: popupString('Bypass redirect')});
+
// note: tries to get images in infobox templates too, e.g. movie pages, album pages etc
}
+
//   (^|\[\[)image: *([^|\]]*[^|\] ]) *
 +
//   (^|\[\[)image: *([^|\]]*[^|\] ])([^0-9\]]*([0-9]+) *px)?
 +
// $4 = 120 as in 120px
 +
pg.re.image = RegExp(
 +
'(^|\\[\\[)' +
 +
im +
 +
': *([^|\\]]*[^|\\] ])' +
 +
'([^0-9\\]]*([0-9]+) *px)?|(?:\\n *[|]?|[|]) *' +
 +
'(' +
 +
getValueOf('popupImageVarsRegexp') +
 +
')' +
 +
' *= *(?:\\[\\[ *)?(?:' +
 +
im +
 +
':)?' +
 +
'([^|]*?)(?:\\]\\])? *[|]? *\\n',
 +
'img'
 +
);
 +
pg.re.imageBracketCount = 6;
  
function arinLink(l) {
+
pg.re.category = RegExp('\\[\\[' + nsRe(pg.nsCategoryId) + ': *([^|\\]]*[^|\\] ]) *', 'i');
if (!saneLinkCheck(l)) { return null; }
+
pg.re.categoryBracketCount = 1;
if ( ! l.article.isIpUser() || ! pg.wiki.wikimedia) return null;
 
  
var uN=l.article.userName();
+
pg.re.ipUser = RegExp(
 +
'^' +
 +
// IPv6
 +
'(?::(?::|(?::[0-9A-Fa-f]{1,4}){1,7})|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}::|[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){7})' +
 +
// IPv4
 +
'|(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' +
 +
'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]))$'
 +
);
  
return generalNavLink({url:'http://ws.arin.net/cgi-bin/whois.pl?queryinput=' + encodeURIComponent(uN), newWin:l.newWin,
+
pg.re.stub = RegExp(getValueOf('popupStubRegexp'), 'im');
title: tprintf('Look up %s in ARIN whois database', [uN]),
+
pg.re.disambig = RegExp(getValueOf('popupDabRegexp'), 'im');
text: l.text, noPopup:1});
 
}
 
  
function toolDbName(cookieStyle) {
+
//</NOLITE>
var ret = mw.config.get('wgDBname');
+
// FIXME replace with general parameter parsing function, this is daft
if (!cookieStyle) { ret+= '_p'; }
+
pg.re.oldid = RegExp('[?&]oldid=([^&]*)');
return ret;
+
pg.re.diff = RegExp('[?&]diff=([^&]*)');
}
+
}
  
function saneLinkCheck(l) {
+
//////////////////////////////////////////////////
if (typeof l.article != typeof {} || typeof l.text != typeof '') { return false; }
+
// miscellany
return true;
 
}
 
function editCounterLink(l) {
 
if(!saneLinkCheck(l)) return null;
 
if (! pg.wiki.wikimedia) return null;
 
var uN=l.article.userName();
 
var tool=getValueOf('popupEditCounterTool');
 
var url;
 
var soxredToolUrl='//tools.wmflabs.org/supercount/index.php?user=$1&project=$2.$3';
 
  
switch(tool) {
+
function setupCache() {
case 'custom':
+
// page caching
url=simplePrintf(getValueOf('popupEditCounterUrl'), [ encodeURIComponent(uN), toolDbName() ]);
+
pg.cache.pages = [];
break;
 
case 'kate':
 
case 'interiot':
 
/* fall through */
 
default:
 
var theWiki=pg.wiki.hostname.split('.');
 
url=simplePrintf(soxredToolUrl, [ encodeURIComponent(uN), theWiki[0], theWiki[1] ]);
 
 
}
 
}
return generalNavLink({url:url, title: tprintf('editCounterLinkHint', [uN]),
 
newWin:l.newWin, text: l.text, noPopup:1});
 
}
 
  
 +
function setMisc() {
 +
pg.current.link = null;
 +
pg.current.links = [];
 +
pg.current.linksHash = {};
  
function globalSearchLink(l) {
+
setupCache();
if(!saneLinkCheck(l)) return null;
 
  
var base='http://vs.aka-online.de/cgi-bin/globalwpsearch.pl?timeout=120&search=';
+
pg.timer.checkPopupPosition = null;
var article=l.article.urlString({keepSpaces:true});
+
pg.counter.loop = 0;
  
return generalNavLink({url:base + article, newWin:l.newWin,
+
// ids change with each popup: popupImage0, popupImage1 etc
title: tprintf('globalSearchHint', [safeDecodeURI(l.article)]),
+
pg.idNumber = 0;
text: l.text, noPopup:1});
 
}
 
  
function googleLink(l) {
+
// for myDecodeURI
if(!saneLinkCheck(l)) return null;
+
pg.misc.decodeExtras = [
 +
{ from: '%2C', to: ',' },
 +
{ from: '_', to: ' ' },
 +
{ from: '%24', to: '$' },
 +
{ from: '%26', to: '&' }, // no ,
 +
];
 +
}
  
var base='http://www.google.com/search?q=';
+
function getMwApi() {
var article=l.article.urlString({keepSpaces:true});
+
if (!pg.api.client) {
 +
pg.api.userAgent = 'Navigation popups/1.0 (' + mw.config.get('wgServerName') + ')';
 +
pg.api.client = new mw.Api({
 +
ajax: {
 +
headers: {
 +
'Api-User-Agent': pg.api.userAgent,
 +
},
 +
},
 +
});
 +
}
 +
return pg.api.client;
 +
}
  
return generalNavLink({url:base + '%22' + article + '%22', newWin:l.newWin,
+
// We need a callback since this might end up asynchronous because of
title: tprintf('googleSearchHint', [safeDecodeURI(l.article)]),
+
// the mw.loader.using() call.
text: l.text, noPopup:1});
+
function setupPopups(callback) {
}
+
if (setupPopups.completed) {
 +
if (typeof callback === 'function') {
 +
callback();
 +
}
 +
return;
 +
}
 +
// These dependencies should alse be enforced from the gadget,
 +
// but not everyone loads this as a gadget, so double check
 +
mw.loader
 +
.using([
 +
'mediawiki.util',
 +
'mediawiki.api',
 +
'mediawiki.user',
 +
'user.options',
 +
'mediawiki.jqueryMsg',
 +
])
 +
.then(fetchSpecialPageNames)
 +
.then(function () {
 +
// NB translatable strings should be set up first (strings.js)
 +
// basics
 +
setupDebugging();
 +
setSiteInfo();
 +
setTitleBase();
 +
setOptions(); // see options.js
 +
setUserInfo();
  
function editorListLink(l) {
+
// namespaces etc
if(!saneLinkCheck(l)) return null;
+
setNamespaces();
var article= l.article.articleFromTalkPage() || l.article;
+
setInterwiki();
var theWiki=pg.wiki.hostname.split('.');
 
var base='//tools.wmflabs.org/xtools/articleinfo/index.php?&uselang=' + mw.config.get('wgUserLanguage') +
 
'lang=' + theWiki[0] + '&wiki=' + theWiki[1] + '&begin=&end=&article=';
 
return generalNavLink({url:base+article.urlString(),
 
title: tprintf('editorListHint', [article]),
 
newWin:l.newWin, text: l.text, noPopup:1});
 
}
 
  
function generalNavLink(l) {
+
// regexps
l.className = (l.className === null) ? 'popupNavLink' : l.className;
+
setRegexps();
return generalLink(l);
+
setRedirs();
}
 
  
//////////////////////////////////////////////////
+
// other stuff
// magic history links
+
setMisc();
//
+
setupLivePreview();
  
function getHistoryInfo(wikipage, whatNext) {
+
// main deal here
log('getHistoryInfo');
+
setupTooltips();
getHistory(wikipage, whatNext ? function(d){whatNext(processHistory(d));} : processHistory);
+
log('In setupPopups(), just called setupTooltips()');
}
+
Navpopup.tracker.enable();
  
// FIXME eliminate pg.idNumber ... how? :-(
+
setupPopups.completed = true;
 +
if (typeof callback === 'function') {
 +
callback();
 +
}
 +
});
 +
}
 +
// ENDFILE: init.js
  
function getHistory(wikipage, onComplete) {
+
// STARTFILE: navlinks.js
log('getHistory');
+
//<NOLITE>
if( !mw.config.get('wgEnableAPI') ) {
+
//////////////////////////////////////////////////
alert( 'This function of navigation popups now requires a MediaWiki ' +
+
// navlinks... let the fun begin
'installation with the API enabled.');
+
//
return false;
 
}
 
var url = pg.wiki.apiwikibase + '?format=json&action=query&prop=revisions&titles=' +
 
new Title(wikipage).urlString() + '&rvlimit=' + getValueOf('popupHistoryLimit');
 
log('getHistory: url='+url);
 
if (pg.flag.isIE) {
 
url = url + '&*'; //to circumvent https://bugzilla.wikimedia.org/show_bug.cgi?id=28840
 
}
 
return startDownload(url, pg.idNumber+'history', onComplete);
 
}
 
  
function processHistory(download) {
+
function defaultNavlinkSpec() {
var jsobj = getJsObj(download.data);
+
var str = '';
try {
+
str += '<b><<mainlink|shortcut= >></b>';
window.x=jsobj;
+
if (getValueOf('popupLastEditLink')) {
var revisions = anyChild(jsobj.query.pages).revisions;
+
str +=
var edits=[];
+
'*<<lastEdit|shortcut=/>>|<<lastContrib>>|<<sinceMe>>if(oldid){|<<oldEdit>>|<<diffCur>>}';
for (var i=0; i<revisions.length; ++i) {
 
edits.push({ oldid: revisions[i].revid, editor: revisions[i].user });
 
 
}
 
}
log('processed ' + edits.length + ' edits');
 
return finishProcessHistory( edits, mw.config.get('wgUserName') );
 
} catch (someError) {
 
log('Something went wrong with JSON business');
 
return finishProcessHistory([]);
 
}
 
}
 
  
 +
// user links
 +
// contribs - log - count - email - block
 +
// count only if applicable; block only if popupAdminLinks
 +
str += 'if(user){<br><<contribs|shortcut=c>>*<<userlog|shortcut=L|log>>';
 +
str += 'if(ipuser){*<<arin>>}if(wikimedia){*<<count|shortcut=#>>}';
 +
str +=
 +
'if(ipuser){}else{*<<email|shortcut=E>>}if(admin){*<<block|shortcut=b>>|<<blocklog|log>>}}';
 +
 +
// editing links
 +
// talkpage  -> edit|new - history - un|watch - article|edit
 +
// other page -> edit - history - un|watch - talk|edit|new
 +
var editstr = '<<edit|shortcut=e>>';
 +
var editOldidStr =
 +
'if(oldid){<<editOld|shortcut=e>>|<<revert|shortcut=v|rv>>|<<edit|cur>>}else{' +
 +
editstr +
 +
'}';
 +
var historystr = '<<history|shortcut=h>>|<<editors|shortcut=E|>>';
 +
var watchstr = '<<unwatch|unwatchShort>>|<<watch|shortcut=w|watchThingy>>';
  
function finishProcessHistory(edits, userName) {
+
str +=
var histInfo={};
+
'<br>if(talk){' +
 +
editOldidStr +
 +
'|<<new|shortcut=+>>' +
 +
'*' +
 +
historystr +
 +
'*' +
 +
watchstr +
 +
'*' +
 +
'<b><<article|shortcut=a>></b>|<<editArticle|edit>>' +
 +
'}else{' + // not a talk page
 +
editOldidStr +
 +
'*' +
 +
historystr +
 +
'*' +
 +
watchstr +
 +
'*' +
 +
'<b><<talk|shortcut=t>></b>|<<editTalk|edit>>|<<newTalk|shortcut=+|new>>}';
  
histInfo.edits=edits;
+
// misc links
histInfo.userName=userName;
+
str += '<br><<whatLinksHere|shortcut=l>>*<<relatedChanges|shortcut=r>>*<<move|shortcut=m>>';
  
for (var i=0; i<edits.length; ++i) {
+
// admin links
if (typeof histInfo.myLastEdit === 'undefined' && userName && edits[i].editor==userName) {
+
str +=
histInfo.myLastEdit={index: i, oldid: edits[i].oldid, previd: (i === 0 ? null : edits[i-1].oldid)};
+
'if(admin){<br><<unprotect|unprotectShort>>|<<protect|shortcut=p>>|<<protectlog|log>>*' +
}
+
'<<undelete|undeleteShort>>|<<delete|shortcut=d>>|<<deletelog|log>>}';
if (typeof histInfo.firstNewEditor === 'undefined' && edits[i].editor != edits[0].editor) {
+
return str;
histInfo.firstNewEditor={index:i, oldid:edits[i].oldid, previd: (i === 0 ? null : edits[i-1].oldid)};
 
}
 
 
}
 
}
//pg.misc.historyInfo=histInfo;
 
return histInfo;
 
}
 
//</NOLITE>
 
// ENDFILE: links.js
 
// STARTFILE: options.js
 
//////////////////////////////////////////////////
 
// options
 
  
// check for cookies and existing value, else use default
+
function navLinksHTML(article, hint, params) {
function defaultize(x) {
+
//oldid, rcid) {
var val=null;
+
var str = '<span class="popupNavLinks">' + defaultNavlinkSpec() + '</span>';
if (x!='popupCookies') {
+
// BAM
defaultize('popupCookies');
+
return navlinkStringToHTML(str, article, params);
if (pg.option.popupCookies && (val=Cookie.read(x))) {
 
pg.option[x]=val;
 
return;
 
}
 
}
 
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) {
+
function expandConditionalNavlinkString(s, article, z, recursionCount) {
pg.optionDefault[x]=def;
+
var oldid = z.oldid,
}
+
rcid = z.rcid,
 +
diff = z.diff;
 +
// nested conditionals (up to 10 deep) are ok, hopefully! (work from the inside out)
 +
if (typeof recursionCount != typeof 0) {
 +
recursionCount = 0;
 +
}
 +
var conditionalSplitRegex = RegExp(
 +
//(1 if \\( (2 2) \\)   {(3 3)}  (4  else   {(5 5)}  4)1)
 +
'(;?\\s*if\\s*\\(\\s*([\\w]*)\\s*\\)\\s*\\{([^{}]*)\\}(\\s*else\\s*\\{([^{}]*?)\\}|))',
 +
'i'
 +
);
 +
var splitted = s.parenSplit(conditionalSplitRegex);
 +
// $1: whole conditional
 +
// $2: test condition
 +
// $3: true expansion
 +
// $4: else clause (possibly empty)
 +
// $5: false expansion (possibly null)
 +
var numParens = 5;
 +
var ret = splitted[0];
 +
for (var i = 1; i < splitted.length; i = i + numParens + 1) {
 +
var testString = splitted[i + 2 - 1];
 +
var trueString = splitted[i + 3 - 1];
 +
var falseString = splitted[i + 5 - 1];
 +
if (typeof falseString == 'undefined' || !falseString) {
 +
falseString = '';
 +
}
 +
var testResult = null;
  
function setDefault(x, def) {
+
switch (testString) {
return newOption(x, def);
+
case 'user':
}
+
testResult = article.userName() ? true : false;
 +
break;
 +
case 'talk':
 +
testResult = article.talkPage() ? false : true; // talkPage converts _articles_ to talkPages
 +
break;
 +
case 'admin':
 +
testResult = getValueOf('popupAdminLinks') ? true : false;
 +
break;
 +
case 'oldid':
 +
testResult = typeof oldid != 'undefined' && oldid ? true : false;
 +
break;
 +
case 'rcid':
 +
testResult = typeof rcid != 'undefined' && rcid ? true : false;
 +
break;
 +
case 'ipuser':
 +
testResult = article.isIpUser() ? true : false;
 +
break;
 +
case 'mainspace_en':
 +
testResult = isInMainNamespace(article) && pg.wiki.hostname == 'en.wikipedia.org';
 +
break;
 +
case 'wikimedia':
 +
testResult = pg.wiki.wikimedia ? true : false;
 +
break;
 +
case 'diff':
 +
testResult = typeof diff != 'undefined' && diff ? true : false;
 +
break;
 +
}
  
function getValueOf(varName) {
+
switch (testResult) {
defaultize(varName);
+
case null:
return pg.option[varName];
+
ret += splitted[i];
}
+
break;
 +
case true:
 +
ret += trueString;
 +
break;
 +
case false:
 +
ret += falseString;
 +
break;
 +
}
  
function useDefaultOptions() { // for testing
+
// append non-conditional string
for (var p in pg.optionDefault) {
+
ret += splitted[i + numParens];
pg.option[p]=pg.optionDefault[p];
+
}
if (typeof window[p]!='undefined') { delete window[p]; }
+
if (conditionalSplitRegex.test(ret) && recursionCount < 10) {
 +
return expandConditionalNavlinkString(ret, article, z, recursionCount + 1);
 +
}
 +
return ret;
 
}
 
}
}
 
  
function setOptions() {
+
function navlinkStringToArray(s, article, params) {
// user-settable parameters and defaults
+
s = expandConditionalNavlinkString(s, article, params);
var userIsSysop = false;
+
var splitted = s.parenSplit(RegExp('<<(.*?)>>'));
if ( mw.config.get('wgUserGroups') ) {
+
var ret = [];
for ( var g = 0; g < mw.config.get('wgUserGroups').length; ++g ) {
+
for (var i = 0; i < splitted.length; ++i) {
if ( mw.config.get('wgUserGroups')[g] == "sysop" )
+
if (i % 2) {
userIsSysop = true;
+
// i odd, so s is a tag
 +
var t = new navlinkTag();
 +
var ss = splitted[i].split('|');
 +
t.id = ss[0];
 +
for (var j = 1; j < ss.length; ++j) {
 +
var sss = ss[j].split('=');
 +
if (sss.length > 1) {
 +
t[sss[0]] = sss[1];
 +
} else {
 +
// no assignment (no "="), so treat this as a title (overwriting the last one)
 +
t.text = popupString(sss[0]);
 +
}
 +
}
 +
t.article = article;
 +
var oldid = params.oldid,
 +
rcid = params.rcid,
 +
diff = params.diff;
 +
if (typeof oldid !== 'undefined' && oldid !== null) {
 +
t.oldid = oldid;
 +
}
 +
if (typeof rcid !== 'undefined' && rcid !== null) {
 +
t.rcid = rcid;
 +
}
 +
if (typeof diff !== 'undefined' && diff !== null) {
 +
t.diff = diff;
 +
}
 +
if (!t.text && t.id !== 'mainlink') {
 +
t.text = popupString(t.id);
 +
}
 +
ret.push(t);
 +
} else {
 +
// plain HTML
 +
ret.push(splitted[i]);
 +
}
 
}
 
}
 +
return ret;
 
}
 
}
  
// Basic options
+
function navlinkSubstituteHTML(s) {
newOption('popupDelay',              0.5);
+
return s
newOption('popupHideDelay',          0.5);
+
.split('*')
newOption('simplePopups',            false);
+
.join(getValueOf('popupNavLinkSeparator'))
newOption('popupStructure',          'shortmenus');  // see later - default for popupStructure is 'original' if simplePopups is true
+
.split('<menurow>')
newOption('popupActionsMenu',        true);
+
.join('<li class="popup_menu_row">')
newOption('popupSetupMenu',          true);
+
.split('</menurow>')
newOption('popupAdminLinks',          userIsSysop);
+
.join('</li>')
newOption('popupShortcutKeys',        false);
+
.split('<menu>')
newOption('popupHistoricalLinks',    true);
+
.join('<ul class="popup_menu">')
newOption('popupOnlyArticleLinks',    true);
+
.split('</menu>')
newOption('removeTitles',            true);
+
.join('</ul>');
newOption('popupMaxWidth',            350);
+
}
newOption('popupInitialWidth',        false); // integer or false
 
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('popupAdjustDiffDates',    true);
 
newOption('popupActiveNavlinks',      true);
 
newOption('popupModifier',            false); // ctrl, shift, alt or meta
 
newOption('popupModifierAction',      'enable'); // or 'disable'
 
newOption('popupDraggable',          true);
 
  
//<NOLITE>
+
function navlinkDepth(magic, s) {
// images
+
return s.split('<' + magic + '>').length - s.split('</' + magic + '>').length;
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
+
// navlinkString: * becomes the separator
newOption('popupFixRedirs',            false);
+
// <<foo|bar=baz|fubar>> becomes a foo-link with attribute bar='baz'
newOption('popupRedirAutoClick',        'wpDiff');
+
//   and visible text 'fubar'
newOption('popupFixDabs',              false);
+
// if(test){...} and if(test){...}else{...} work too (nested ok)
newOption('popupDabsAutoClick',        'wpDiff');
 
newOption('popupRevertSummaryPrompt',  false);
 
newOption('popupMinorReverts',          false);
 
newOption('popupRedlinkRemoval',        false);
 
newOption('popupWatchDisambiggedPages', null);
 
newOption('popupWatchRedirredPages',    null);
 
newOption('popupDabWiktionary',        'last');
 
  
// navlinks
+
function navlinkStringToHTML(s, article, params) {
newOption('popupNavLinks',         true);
+
//limitAlert(navlinkStringToHTML, 5, 'navlinkStringToHTML\n' + article + '\n' + (typeof article));
newOption('popupNavLinkSeparator', ' &sdot; ');
+
var p = navlinkStringToArray(s, article, params);
newOption('popupLastEditLink',     true);
+
var html = '';
newOption('popupEditCounterTool',   'soxred');
+
var menudepth = 0; // nested menus not currently allowed, but doesn't do any harm to code for it
newOption('popupEditCounterUrl',    '');
+
var menurowdepth = 0;
newOption('popupExtraUserMenu',    '');
+
for (var i = 0; i < p.length; ++i) {
//</NOLITE>
+
if (typeof p[i] == typeof '') {
 +
html += navlinkSubstituteHTML(p[i]);
 +
menudepth += navlinkDepth('menu', p[i]);
 +
menurowdepth += navlinkDepth('menurow', p[i]);
 +
// if (menudepth === 0) {
 +
// tagType='span';
 +
// } else if (menurowdepth === 0) {
 +
// tagType='li';
 +
// } else {
 +
// tagType = null;
 +
// }
 +
} else if (typeof p[i].type != 'undefined' && p[i].type == 'navlinkTag') {
 +
if (menudepth > 0 && menurowdepth === 0) {
 +
html += '<li class="popup_menu_item">' + p[i].html() + '</li>';
 +
} else {
 +
html += p[i].html();
 +
}
 +
}
 +
}
 +
return html;
 +
}
  
// previews etc
+
function navlinkTag() {
newOption('popupPreviews',            true);
+
this.type = 'navlinkTag';
newOption('popupSummaryData',          true);
+
}
newOption('popupMaxPreviewSentences',  5);
 
newOption('popupMaxPreviewCharacters', 600);
 
newOption('popupLastModified',        true);
 
newOption('popupPreviewKillTemplates', true);
 
newOption('popupPreviewRawTemplates',  true);
 
newOption('popupPreviewFirstParOnly',  true);
 
newOption('popupPreviewCutHeadings',  true);
 
newOption('popupPreviewButton',        false);
 
newOption('popupPreviewButtonEvent',  'click');
 
  
//<NOLITE>
+
navlinkTag.prototype.html = function () {
// diffs
+
this.getNewWin();
newOption('popupPreviewDiffs',         true);
+
this.getPrintFunction();
newOption('popupDiffMaxLines',          100);
+
var html = '';
newOption('popupDiffContextLines',      2);
+
var opening, closing;
newOption('popupDiffContextCharacters', 40);
+
var tagType = 'span';
newOption('popupDiffDates',            true);
+
if (!tagType) {
newOption('popupDiffDatePrinter',       'toLocaleString');
+
opening = '';
 +
closing = '';
 +
} else {
 +
opening = '<' + tagType + ' class="popup_' + this.id + '">';
 +
closing = '</' + tagType + '>';
 +
}
 +
if (typeof this.print != 'function') {
 +
errlog('Oh dear - invalid print function for a navlinkTag, id=' + this.id);
 +
} else {
 +
html = this.print(this);
 +
if (typeof html != typeof '') {
 +
html = '';
 +
} else if (typeof this.shortcut != 'undefined') html = addPopupShortcut(html, this.shortcut);
 +
}
 +
return opening + html + closing;
 +
};
  
// edit summaries. God, these are ugly.
+
navlinkTag.prototype.getNewWin = function () {
newOption('popupFixDabsSummary',          popupString('defaultpopupFixDabsSummary') );
+
getValueOf('popupLinksNewWindow');
newOption('popupExtendedRevertSummary',    popupString('defaultpopupExtendedRevertSummary') );
+
if (typeof pg.option.popupLinksNewWindow[this.id] === 'undefined') {
newOption('popupTimeOffset',              null);
+
this.newWin = null;
newOption('popupRevertSummary',            popupString('defaultpopupRevertSummary') );
+
}
newOption('popupRevertToPreviousSummary',  popupString('defaultpopupRevertToPreviousSummary') );
+
this.newWin = pg.option.popupLinksNewWindow[this.id];
newOption('popupQueriedRevertSummary',            popupString('defaultpopupQueriedRevertSummary') );
+
};
newOption('popupQueriedRevertToPreviousSummary',  popupString('defaultpopupQueriedRevertToPreviousSummary') );
 
newOption('popupFixRedirsSummary',        popupString('defaultpopupFixRedirsSummary') );
 
newOption('popupRedlinkSummary',          popupString('defaultpopupRedlinkSummary') );
 
newOption('popupRmDabLinkSummary',        popupString('defaultpopupRmDabLinkSummary') );
 
//</NOLITE>
 
// misc
 
newOption('popupCookies',            false);
 
newOption('popupHistoryLimit',        50);
 
//<NOLITE>
 
newOption('popupFilters',            [popupFilterStubDetect,    popupFilterDisambigDetect,
 
      popupFilterPageSize,      popupFilterCountLinks,
 
      popupFilterCountImages,    popupFilterCountCategories,
 
      popupFilterLastModified]);
 
newOption('extraPopupFilters',        []);
 
newOption('popupOnEditSelection', 'cursor');
 
newOption('popupPreviewHistory',      true);
 
newOption('popupImageLinks',          true);
 
newOption('popupCategoryMembers',    true);
 
newOption('popupUserInfo',            true);
 
newOption('popupHistoryPreviewLimit', 25);
 
newOption('popupContribsPreviewLimit',25);
 
newOption('popupRevDelUrl',          '//en.wikipedia.org/wiki/Wikipedia:Revision_deletion');
 
newOption('popupShowGender',          true);
 
//</NOLITE>
 
  
// new windows
+
navlinkTag.prototype.getPrintFunction = function () {
newOption('popupNewWindows',    false);
+
//think about this some more
newOption('popupLinksNewWindow', {'lastContrib': true, 'sinceMe': true});
+
// this.id and this.article should already be defined
 +
if (typeof this.id != typeof '' || typeof this.article != typeof {}) {
 +
return;
 +
}
  
// regexps
+
this.noPopup = 1;
newOption('popupDabRegexp', '(\\{\\{\\s*disambig(?!uation needed)|disambig(uation|)\\s*\\}\\}|disamb\\s*\\}\\}|dab\\s*\\}\\})|\\{\\{\\s*(((geo|hn|road?|school|number)dis)|[234][lc][acw]|(road|ship)index)(\\s*[|][^}]*)?\\s*[}][}]|is a .*disambiguation.*page');
+
switch (this.id) {
newOption('popupAnchorRegexp', 'anchors?'); //how to identify an anchors template
+
case 'contribs':
newOption('popupStubRegexp', '(sect)?stub[}][}]|This .*-related article is a .*stub');
+
case 'history':
newOption('popupImageVarsRegexp', 'image|image_(?:file|skyline|name|flag|seal)|cover|badge|logo');
+
case 'whatLinksHere':
}
+
case 'userPage':
// ENDFILE: options.js
+
case 'monobook':
// STARTFILE: strings.js
+
case 'userTalk':
//<NOLITE>
+
case 'talk':
//////////////////////////////////////////////////
+
case 'article':
// Translatable strings
+
case 'lastEdit':
//////////////////////////////////////////////////
+
this.noPopup = null;
//
+
}
// See instructions at
+
switch (this.id) {
// http://en.wikipedia.org/wiki/Wikipedia:Tools/Navigation_popups/Translation
+
case 'email':
 +
case 'contribs':
 +
case 'block':
 +
case 'unblock':
 +
case 'userlog':
 +
case 'userSpace':
 +
case 'deletedContribs':
 +
this.article = this.article.userName();
 +
}
  
pg.string = {
+
switch (this.id) {
/////////////////////////////////////
+
case 'userTalk':
// summary data, searching etc.
+
case 'newUserTalk':
/////////////////////////////////////
+
case 'editUserTalk':
'article': 'article',
+
case 'userPage':
'category': 'category',
+
case 'monobook':
'categories': 'categories',
+
case 'editMonobook':
'image': 'image',
+
case 'blocklog':
'images': 'images',
+
this.article = this.article.userName(true);
'stub': 'stub',
+
/* fall through */
'section stub': 'section stub',
+
case 'pagelog':
'Empty page': 'Empty page',
+
case 'deletelog':
'kB': 'kB',
+
case 'protectlog':
'bytes': 'bytes',
+
delete this.oldid;
'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&nbsp;revision': 'this&nbsp;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',
 
'ThanksHint': 'Send a thank you notification to this user',
 
'move': 'move',
 
'move page': 'move page',
 
'MovepageHint': 'Change the title of %s',
 
'edit': 'edit',   ///// edit articles and talk
 
'edit article': 'edit article',
 
'editHint': 'Change the content of %s',
 
'edit talk': 'edit talk',
 
'new': 'new',
 
'new topic': 'new topic',
 
'newSectionHint': 'Start a new section on %s',
 
'null edit': 'null edit',
 
'nullEditHint': 'Submit an edit to %s, making no changes ',
 
'hist': 'hist',   ///// history, diffs, editors, related
 
'history': 'history',
 
'historyHint': 'List the changes made to %s',
 
'last': 'last',
 
'lastEdit': 'lastEdit',
 
'mark patrolled': 'mark patrolled',
 
'markpatrolledHint': 'Mark 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',
 
'defaultpopupRedlinkSummary': 'Removing link to empty page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupFixDabsSummary': 'Disambiguate [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupFixRedirsSummary': 'Redirect bypass from [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupExtendedRevertSummary': 'Revert to revision dated %s by %s, oldid %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupRevertToPreviousSummary': 'Revert to the revision prior to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupRevertSummary': 'Revert to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupQueriedRevertToPreviousSummary': 'Revert to the revision prior to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupQueriedRevertSummary': 'Revert to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 
'defaultpopupRmDabLinkSummary': 'Remove link to dab page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|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&nbsp;page': 'user&nbsp;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',
 
' edits since: ': ' edits since: ',
 
'last edit on ': 'last edit on ',
 
/////////////////////////////////////
 
// Autoediting
 
/////////////////////////////////////
 
'Enter a non-empty edit summary or press cancel to abort': 'Enter a non-empty edit summary or press cancel to abort',
 
'Failed to get revision information, please edit manually.\n\n': 'Failed to get revision information, please edit manually.\n\n',
 
'The %s button has been automatically clicked. Please wait for the next page to load.': 'The %s button has been automatically clicked. Please wait for the next page to load.',
 
'Could not find button %s. Please check the settings in your javascript file.': 'Could not find button %s. Please check the settings in your javascript file.',
 
/////////////////////////////////////
 
// Popups setup
 
/////////////////////////////////////
 
'Open full-size image': 'Open full-size image',
 
'zxy': 'zxy',
 
'autoedit_version': 'np20140416'
 
};
 
 
 
 
 
function popupString(str) {
 
if (typeof popupStrings != 'undefined' && popupStrings && popupStrings[str]) { return popupStrings[str]; }
 
if (pg.string[str]) { return pg.string[str]; }
 
return str;
 
}
 
 
 
 
 
function tprintf(str,subs) {
 
if (typeof subs != typeof []) { subs = [subs]; }
 
return simplePrintf(popupString(str), subs);
 
}
 
 
 
//</NOLITE>
 
// ENDFILE: strings.js
 
// STARTFILE: run.js
 
////////////////////////////////////////////////////////////////////
 
// Run things
 
////////////////////////////////////////////////////////////////////
 
 
 
 
 
// For some reason popups requires a fully loaded page jQuery.ready(...) causes problems for some.
 
// The old addOnloadHook did something similar to the below
 
if (document.readyState=="complete")
 
autoEdit(); //will setup popups
 
else
 
$( window ).on( 'load', autoEdit );
 
 
 
 
 
// Support for MediaWiki's live preview, VisualEditor's saves and Echo's flyout.
 
( function () {
 
var once = true;
 
function dynamicContentHandler( $content ) {
 
// Try to detect the hook fired on initial page load and disregard
 
// it, we already hook to onload (possibly to different parts of
 
// page - it's configurable) and running twice might be bad. Ugly…
 
if ( $content.attr( 'id' ) == 'mw-content-text' ) {
 
if ( once ) {
 
once = false;
 
return;
 
}
 
 
}
 
}
  
function doIt () {
+
if (this.id == 'editMonobook' || this.id == 'monobook') {
$content.each( function () {
+
this.article.append('/monobook.js');
this.ranSetupTooltipsAlready = false;
 
setupTooltips( this );
 
} );
 
 
}
 
}
  
if ( !setupPopups.completed ) {
+
if (this.id != 'mainlink') {
setupPopups( doIt );
+
// FIXME anchor handling should be done differently with Title object
} else {
+
this.article = this.article.removeAnchor();
doIt();
+
// if (typeof this.text=='undefined') this.text=popupString(this.id);
 
}
 
}
}
 
  
// This hook is also fired after page load.
+
switch (this.id) {
mw.hook( 'wikipage.content' ).add( dynamicContentHandler );
+
case 'undelete':
 
+
this.print = specialLink;
mw.hook( 'ext.echo.overlay.beforeShowingOverlay' ).add( function($overlay){
+
this.specialpage = 'Undelete';
dynamicContentHandler( $overlay.find(".mw-echo-state") );
+
this.sep = '/';
});
+
break;
} )();
+
case 'whatLinksHere':
 +
this.print = specialLink;
 +
this.specialpage = 'Whatlinkshere';
 +
break;
 +
case 'relatedChanges':
 +
this.print = specialLink;
 +
this.specialpage = 'Recentchangeslinked';
 +
break;
 +
case 'move':
 +
this.print = specialLink;
 +
this.specialpage = 'Movepage';
 +
break;
 +
case 'contribs':
 +
this.print = specialLink;
 +
this.specialpage = 'Contributions';
 +
break;
 +
case 'deletedContribs':
 +
this.print = specialLink;
 +
this.specialpage = 'Deletedcontributions';
 +
break;
 +
case 'email':
 +
this.print = specialLink;
 +
this.specialpage = 'EmailUser';
 +
this.sep = '/';
 +
break;
 +
case 'block':
 +
this.print = specialLink;
 +
this.specialpage = 'Blockip';
 +
this.sep = '&ip=';
 +
break;
 +
case 'unblock':
 +
this.print = specialLink;
 +
this.specialpage = 'Ipblocklist';
 +
this.sep = '&action=unblock&ip=';
 +
break;
 +
case 'userlog':
 +
this.print = specialLink;
 +
this.specialpage = 'Log';
 +
this.sep = '&user=';
 +
break;
 +
case 'blocklog':
 +
this.print = specialLink;
 +
this.specialpage = 'Log';
 +
this.sep = '&type=block&page=';
 +
break;
 +
case 'pagelog':
 +
this.print = specialLink;
 +
this.specialpage = 'Log';
 +
this.sep = '&page=';
 +
break;
 +
case 'protectlog':
 +
this.print = specialLink;
 +
this.specialpage = 'Log';
 +
this.sep = '&type=protect&page=';
 +
break;
 +
case 'deletelog':
 +
this.print = specialLink;
 +
this.specialpage = 'Log';
 +
this.sep = '&type=delete&page=';
 +
break;
 +
case 'userSpace':
 +
this.print = specialLink;
 +
this.specialpage = 'PrefixIndex';
 +
this.sep = '&namespace=2&prefix=';
 +
break;
 +
case 'search':
 +
this.print = specialLink;
 +
this.specialpage = 'Search';
 +
this.sep = '&fulltext=Search&search=';
 +
break;
 +
case 'thank':
 +
this.print = specialLink;
 +
this.specialpage = 'Thanks';
 +
this.sep = '/';
 +
this.article.value = this.diff !== 'prev' ? this.diff : this.oldid;
 +
break;
 +
case 'unwatch':
 +
case 'watch':
 +
this.print = magicWatchLink;
 +
this.action =
 +
this.id +
 +
'&autowatchlist=1&autoimpl=' +
 +
popupString('autoedit_version') +
 +
'&actoken=' +
 +
autoClickToken();
 +
break;
 +
case 'history':
 +
case 'historyfeed':
 +
case 'unprotect':
 +
case 'protect':
 +
this.print = wikiLink;
 +
this.action = this.id;
 +
break;
 +
 
 +
case 'delete':
 +
this.print = wikiLink;
 +
this.action = 'delete';
 +
if (this.article.namespaceId() == pg.nsImageId) {
 +
var img = this.article.stripNamespace();
 +
this.action += '&image=' + img;
 +
}
 +
break;
  
 +
case 'markpatrolled':
 +
case 'edit': // editOld should keep the oldid, but edit should not.
 +
delete this.oldid;
 +
/* fall through */
 +
case 'view':
 +
case 'purge':
 +
case 'render':
 +
this.print = wikiLink;
 +
this.action = this.id;
 +
break;
 +
case 'raw':
 +
this.print = wikiLink;
 +
this.action = 'raw';
 +
break;
 +
case 'new':
 +
this.print = wikiLink;
 +
this.action = 'edit&section=new';
 +
break;
 +
case 'mainlink':
 +
if (typeof this.text == 'undefined') {
 +
this.text = this.article.toString().entify();
 +
}
 +
if (getValueOf('popupSimplifyMainLink') && isInStrippableNamespace(this.article)) {
 +
// only show the /subpage part of the title text
 +
var s = this.text.split('/');
 +
this.text = s[s.length - 1];
 +
if (this.text === '' && s.length > 1) {
 +
this.text = s[s.length - 2];
 +
}
 +
}
 +
this.print = titledWikiLink;
 +
if (
 +
typeof this.title === 'undefined' &&
 +
pg.current.link &&
 +
typeof pg.current.link.href !== 'undefined'
 +
) {
 +
this.title = safeDecodeURI(
 +
pg.current.link.originalTitle ? pg.current.link.originalTitle : this.article
 +
);
 +
if (typeof this.oldid !== 'undefined' && this.oldid) {
 +
this.title = tprintf('Revision %s of %s', [this.oldid, this.title]);
 +
}
 +
}
 +
this.action = 'view';
 +
break;
 +
case 'userPage':
 +
case 'article':
 +
case 'monobook':
 +
case 'editMonobook':
 +
case 'editArticle':
 +
delete this.oldid;
 +
//alert(this.id+'\n'+this.article + '\n'+ typeof this.article);
 +
this.article = this.article.articleFromTalkOrArticle();
 +
//alert(this.id+'\n'+this.article + '\n'+ typeof this.article);
 +
this.print = wikiLink;
 +
if (this.id.indexOf('edit') === 0) {
 +
this.action = 'edit';
 +
} else {
 +
this.action = 'view';
 +
}
 +
break;
 +
case 'userTalk':
 +
case 'talk':
 +
this.article = this.article.talkPage();
 +
delete this.oldid;
 +
this.print = wikiLink;
 +
this.action = 'view';
 +
break;
 +
case 'arin':
 +
this.print = arinLink;
 +
break;
 +
case 'count':
 +
this.print = editCounterLink;
 +
break;
 +
case 'google':
 +
this.print = googleLink;
 +
break;
 +
case 'editors':
 +
this.print = editorListLink;
 +
break;
 +
case 'globalsearch':
 +
this.print = globalSearchLink;
 +
break;
 +
case 'lastEdit':
 +
this.print = titledDiffLink;
 +
this.title = popupString('Show the last edit');
 +
this.from = 'prev';
 +
this.to = 'cur';
 +
break;
 +
case 'oldEdit':
 +
this.print = titledDiffLink;
 +
this.title = popupString('Show the edit made to get revision') + ' ' + this.oldid;
 +
this.from = 'prev';
 +
this.to = this.oldid;
 +
break;
 +
case 'editOld':
 +
this.print = wikiLink;
 +
this.action = 'edit';
 +
break;
 +
case 'undo':
 +
this.print = wikiLink;
 +
this.action = 'edit&undo=';
 +
break;
 +
case 'revert':
 +
this.print = wikiLink;
 +
this.action = 'revert';
 +
break;
 +
case 'nullEdit':
 +
this.print = wikiLink;
 +
this.action = 'nullEdit';
 +
break;
 +
case 'diffCur':
 +
this.print = titledDiffLink;
 +
this.title = tprintf('Show changes since revision %s', [this.oldid]);
 +
this.from = this.oldid;
 +
this.to = 'cur';
 +
break;
 +
case 'editUserTalk':
 +
case 'editTalk':
 +
delete this.oldid;
 +
this.article = this.article.talkPage();
 +
this.action = 'edit';
 +
this.print = wikiLink;
 +
break;
 +
case 'newUserTalk':
 +
case 'newTalk':
 +
this.article = this.article.talkPage();
 +
this.action = 'edit&section=new';
 +
this.print = wikiLink;
 +
break;
 +
case 'lastContrib':
 +
case 'sinceMe':
 +
this.print = magicHistoryLink;
 +
break;
 +
case 'togglePreviews':
 +
this.text = popupString(pg.option.simplePopups ? 'enable previews' : 'disable previews');
 +
/* fall through */
 +
case 'disablePopups':
 +
case 'purgePopups':
 +
this.print = popupMenuLink;
 +
break;
 +
default:
 +
this.print = function () {
 +
return 'Unknown navlink type: ' + this.id + '';
 +
};
 +
}
 +
};
 +
//
 +
//  end navlinks
 +
//////////////////////////////////////////////////
 +
//</NOLITE>
 +
// ENDFILE: navlinks.js
 +
 +
// STARTFILE: shortcutkeys.js
 +
//<NOLITE>
 +
function popupHandleKeypress(evt) {
 +
var keyCode = window.event ? window.event.keyCode : evt.keyCode ? evt.keyCode : evt.which;
 +
if (!keyCode || !pg.current.link || !pg.current.link.navpopup) {
 +
return;
 +
}
 +
if (keyCode == 27) {
 +
// escape
 +
killPopup();
 +
return false; // swallow keypress
 +
}
 +
 +
var letter = String.fromCharCode(keyCode);
 +
var links = pg.current.link.navpopup.mainDiv.getElementsByTagName('A');
 +
var startLink = 0;
 +
var i, j;
 +
 +
if (popupHandleKeypress.lastPopupLinkSelected) {
 +
for (i = 0; i < links.length; ++i) {
 +
if (links[i] == popupHandleKeypress.lastPopupLinkSelected) {
 +
startLink = i;
 +
}
 +
}
 +
}
 +
for (j = 0; j < links.length; ++j) {
 +
i = (startLink + j + 1) % links.length;
 +
if (links[i].getAttribute('popupkey') == letter) {
 +
if (evt && evt.preventDefault) evt.preventDefault();
 +
links[i].focus();
 +
popupHandleKeypress.lastPopupLinkSelected = links[i];
 +
return false; // swallow keypress
 +
}
 +
}
 +
 +
// pass keypress on
 +
if (document.oldPopupOnkeypress) {
 +
return document.oldPopupOnkeypress(evt);
 +
}
 +
return true;
 +
}
 +
 +
function addPopupShortcuts() {
 +
if (document.onkeypress != popupHandleKeypress) {
 +
document.oldPopupOnkeypress = document.onkeypress;
 +
}
 +
document.onkeypress = popupHandleKeypress;
 +
}
 +
 +
function rmPopupShortcuts() {
 +
popupHandleKeypress.lastPopupLinkSelected = null;
 +
try {
 +
if (document.oldPopupOnkeypress && document.oldPopupOnkeypress == popupHandleKeypress) {
 +
// panic
 +
document.onkeypress = null; //function () {};
 +
return;
 +
}
 +
document.onkeypress = document.oldPopupOnkeypress;
 +
} catch (nasties) {
 +
/* IE goes here */
 +
}
 +
}
 +
 +
function addLinkProperty(html, property) {
 +
// take "<a href=...>...</a> and add a property
 +
// not sophisticated at all, easily broken
 +
var i = html.indexOf('>');
 +
if (i < 0) {
 +
return html;
 +
}
 +
return html.substring(0, i) + ' ' + property + html.substring(i);
 +
}
 +
 +
function addPopupShortcut(html, key) {
 +
if (!getValueOf('popupShortcutKeys')) {
 +
return html;
 +
}
 +
var ret = addLinkProperty(html, 'popupkey="' + key + '"');
 +
if (key == ' ') {
 +
key = popupString('spacebar');
 +
}
 +
return ret.replace(RegExp('^(.*?)(title=")(.*?)(".*)$', 'i'), '$1$2$3 [' + key + ']$4');
 +
}
 +
//</NOLITE>
 +
// ENDFILE: shortcutkeys.js
 +
 +
// STARTFILE: diffpreview.js
 +
//<NOLITE>
 +
//lets jump through hoops to find the rev ids we need to retrieve
 +
function loadDiff(article, oldid, diff, navpop) {
 +
navpop.diffData = { oldRev: {}, newRev: {} };
 +
mw.loader.using('mediawiki.api').then(function () {
 +
var api = getMwApi();
 +
var params = {
 +
action: 'compare',
 +
prop: 'ids|title',
 +
};
 +
if (article.title) {
 +
params.fromtitle = article.title;
 +
}
 +
 +
switch (diff) {
 +
case 'cur':
 +
switch (oldid) {
 +
case null:
 +
case '':
 +
case 'prev':
 +
// this can only work if we have the title
 +
// cur -> prev
 +
params.torelative = 'prev';
 +
break;
 +
default:
 +
params.fromrev = oldid;
 +
params.torelative = 'cur';
 +
break;
 +
}
 +
break;
 +
case 'prev':
 +
if (oldid) {
 +
params.fromrev = oldid;
 +
} else {
 +
params.fromtitle;
 +
}
 +
params.torelative = 'prev';
 +
break;
 +
case 'next':
 +
params.fromrev = oldid || 0;
 +
params.torelative = 'next';
 +
break;
 +
default:
 +
params.fromrev = oldid || 0;
 +
params.torev = diff || 0;
 +
break;
 +
}
 +
 +
api.get(params).then(function (data) {
 +
navpop.diffData.oldRev.revid = data.compare.fromrevid;
 +
navpop.diffData.newRev.revid = data.compare.torevid;
 +
 +
addReviewLink(navpop, 'popupMiscTools');
 +
 +
var go = function () {
 +
pendingNavpopTask(navpop);
 +
var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';
 +
 +
url += 'revids=' + navpop.diffData.oldRev.revid + '|' + navpop.diffData.newRev.revid;
 +
url += '&prop=revisions&rvprop=ids|timestamp|content';
 +
 +
getPageWithCaching(url, doneDiff, navpop);
 +
 +
return true; // remove hook once run
 +
};
 +
if (navpop.visible || !getValueOf('popupLazyDownloads')) {
 +
go();
 +
} else {
 +
navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_DIFFS');
 +
}
 +
});
 +
});
 +
}
 +
 +
// Put a "mark patrolled" link to an element target
 +
// TODO: Allow patrol a revision, as well as a diff
 +
function addReviewLink(navpop, target) {
 +
if (!pg.user.canReview) return;
 +
// If 'newRev' is older than 'oldRev' than it could be confusing, so we do not show the review link.
 +
if (navpop.diffData.newRev.revid <= navpop.diffData.oldRev.revid) return;
 +
var params = {
 +
action: 'query',
 +
prop: 'info|flagged',
 +
revids: navpop.diffData.oldRev.revid,
 +
formatversion: 2,
 +
};
 +
getMwApi()
 +
.get(params)
 +
.then(function (data) {
 +
var stable_revid =
 +
(data.query.pages[0].flagged && data.query.pages[0].flagged.stable_revid) || 0;
 +
// The diff can be reviewed if the old version is the last reviewed version
 +
// TODO: Other possible conditions that we may want to implement instead of this one:
 +
//  * old version is patrolled and the new version is not patrolled
 +
//  * old version is patrolled and the new version is more recent than the last reviewed version
 +
if (stable_revid == navpop.diffData.oldRev.revid) {
 +
var a = document.createElement('a');
 +
a.innerHTML = popupString('mark patrolled');
 +
a.title = popupString('markpatrolledHint');
 +
a.onclick = function () {
 +
var params = {
 +
action: 'review',
 +
revid: navpop.diffData.newRev.revid,
 +
comment: tprintf('defaultpopupReviewedSummary', [
 +
navpop.diffData.oldRev.revid,
 +
navpop.diffData.newRev.revid,
 +
]),
 +
};
 +
getMwApi()
 +
.postWithToken('csrf', params)
 +
.done(function () {
 +
a.style.display = 'none';
 +
// TODO: Update current page and other already constructed popups
 +
})
 +
.fail(function () {
 +
alert(popupString('Could not marked this edit as patrolled'));
 +
});
 +
};
 +
setPopupHTML(a, target, navpop.idNumber, null, true);
 +
}
 +
});
 +
}
 +
 +
function doneDiff(download) {
 +
if (!download.owner || !download.owner.diffData) {
 +
return;
 +
}
 +
var navpop = download.owner;
 +
completedNavpopTask(navpop);
 +
 +
var pages,
 +
revisions = [];
 +
try {
 +
// Process the downloads
 +
pages = getJsObj(download.data).query.pages;
 +
for (var i = 0; i < pages.length; i++) {
 +
revisions = revisions.concat(pages[i].revisions);
 +
}
 +
for (i = 0; i < revisions.length; i++) {
 +
if (revisions[i].revid == navpop.diffData.oldRev.revid) {
 +
navpop.diffData.oldRev.revision = revisions[i];
 +
} else if (revisions[i].revid == navpop.diffData.newRev.revid) {
 +
navpop.diffData.newRev.revision = revisions[i];
 +
}
 +
}
 +
} catch (someError) {
 +
errlog('Could not get diff');
 +
}
 +
 +
insertDiff(navpop);
 +
}
 +
 +
function rmBoringLines(a, b, context) {
 +
if (typeof context == 'undefined') {
 +
context = 2;
 +
}
 +
// this is fairly slow... i think it's quicker than doing a word-based diff from the off, though
 +
var aa = [],
 +
aaa = [];
 +
var bb = [],
 +
bbb = [];
 +
var i, j;
 +
 +
// first, gather all disconnected nodes in a and all crossing nodes in a and b
 +
for (i = 0; i < a.length; ++i) {
 +
if (!a[i].paired) {
 +
aa[i] = 1;
 +
} else if (countCrossings(b, a, i, true)) {
 +
aa[i] = 1;
 +
bb[a[i].row] = 1;
 +
}
 +
}
 +
 +
// pick up remaining disconnected nodes in b
 +
for (i = 0; i < b.length; ++i) {
 +
if (bb[i] == 1) {
 +
continue;
 +
}
 +
if (!b[i].paired) {
 +
bb[i] = 1;
 +
}
 +
}
 +
 +
// another pass to gather context: we want the neighbours of included nodes which are not
 +
// yet included we have to add in partners of these nodes, but we don't want to add context
 +
// for *those* nodes in the next pass
 +
for (i = 0; i < b.length; ++i) {
 +
if (bb[i] == 1) {
 +
for (j = Math.max(0, i - context); j < Math.min(b.length, i + context); ++j) {
 +
if (!bb[j]) {
 +
bb[j] = 1;
 +
aa[b[j].row] = 0.5;
 +
}
 +
}
 +
}
 +
}
 +
 +
for (i = 0; i < a.length; ++i) {
 +
if (aa[i] == 1) {
 +
for (j = Math.max(0, i - context); j < Math.min(a.length, i + context); ++j) {
 +
if (!aa[j]) {
 +
aa[j] = 1;
 +
bb[a[j].row] = 0.5;
 +
}
 +
}
 +
}
 +
}
 +
 +
for (i = 0; i < bb.length; ++i) {
 +
if (bb[i] > 0) {
 +
// it's a row we need
 +
if (b[i].paired) {
 +
bbb.push(b[i].text);
 +
} // joined; partner should be in aa
 +
else {
 +
bbb.push(b[i]);
 +
}
 +
}
 +
}
 +
for (i = 0; i < aa.length; ++i) {
 +
if (aa[i] > 0) {
 +
// it's a row we need
 +
if (a[i].paired) {
 +
aaa.push(a[i].text);
 +
} // joined; partner should be in aa
 +
else {
 +
aaa.push(a[i]);
 +
}
 +
}
 +
}
 +
 +
return { a: aaa, b: bbb };
 +
}
 +
 +
function stripOuterCommonLines(a, b, context) {
 +
var i = 0;
 +
while (i < a.length && i < b.length && a[i] == b[i]) {
 +
++i;
 +
}
 +
var j = a.length - 1;
 +
var k = b.length - 1;
 +
while (j >= 0 && k >= 0 && a[j] == b[k]) {
 +
--j;
 +
--k;
 +
}
 +
 +
return {
 +
a: a.slice(Math.max(0, i - 1 - context), Math.min(a.length + 1, j + context + 1)),
 +
b: b.slice(Math.max(0, i - 1 - context), Math.min(b.length + 1, k + context + 1)),
 +
};
 +
}
 +
 +
function insertDiff(navpop) {
 +
// for speed reasons, we first do a line-based diff, discard stuff that seems boring, then
 +
// do a word-based diff
 +
// FIXME: sometimes this gives misleading diffs as distant chunks are squashed together
 +
var oldlines = navpop.diffData.oldRev.revision.content.split('\n');
 +
var newlines = navpop.diffData.newRev.revision.content.split('\n');
 +
var inner = stripOuterCommonLines(oldlines, newlines, getValueOf('popupDiffContextLines'));
 +
oldlines = inner.a;
 +
newlines = inner.b;
 +
var truncated = false;
 +
getValueOf('popupDiffMaxLines');
 +
if (
 +
oldlines.length > pg.option.popupDiffMaxLines ||
 +
newlines.length > pg.option.popupDiffMaxLines
 +
) {
 +
// truncate
 +
truncated = true;
 +
inner = stripOuterCommonLines(
 +
oldlines.slice(0, pg.option.popupDiffMaxLines),
 +
newlines.slice(0, pg.option.popupDiffMaxLines),
 +
pg.option.popupDiffContextLines
 +
);
 +
oldlines = inner.a;
 +
newlines = inner.b;
 +
}
 +
 +
var lineDiff = diff(oldlines, newlines);
 +
var lines2 = rmBoringLines(lineDiff.o, lineDiff.n);
 +
var oldlines2 = lines2.a;
 +
var newlines2 = lines2.b;
 +
 +
var simpleSplit = !String.prototype.parenSplit.isNative;
 +
var html = '<hr />';
 +
if (getValueOf('popupDiffDates')) {
 +
html += diffDatesTable(navpop);
 +
html += '<hr />';
 +
}
 +
html += shortenDiffString(
 +
diffString(oldlines2.join('\n'), newlines2.join('\n'), simpleSplit),
 +
getValueOf('popupDiffContextCharacters')
 +
).join('<hr />');
 +
setPopupTipsAndHTML(
 +
html.split('\n').join('<br>') +
 +
(truncated
 +
? '<hr /><b>' + popupString('Diff truncated for performance reasons') + '</b>'
 +
: ''),
 +
'popupPreview',
 +
navpop.idNumber
 +
);
 +
}
 +
 +
function diffDatesTable(navpop) {
 +
var html = '<table class="popup_diff_dates">';
 +
html += diffDatesTableRow(navpop.diffData.newRev.revision, tprintf('New revision'));
 +
html += diffDatesTableRow(navpop.diffData.oldRev.revision, tprintf('Old revision'));
 +
html += '</table>';
 +
return html;
 +
}
 +
function diffDatesTableRow(revision, label) {
 +
var txt = '';
 +
var lastModifiedDate = new Date(revision.timestamp);
 +
 +
txt = formattedDateTime(lastModifiedDate);
 +
 +
var revlink = generalLink({
 +
url: mw.config.get('wgScript') + '?oldid=' + revision.revid,
 +
text: label,
 +
title: label,
 +
});
 +
return simplePrintf('<tr><td>%s</td><td>%s</td></tr>', [revlink, txt]);
 +
}
 +
//</NOLITE>
 +
// ENDFILE: diffpreview.js
 +
 +
// STARTFILE: links.js
 +
//<NOLITE>
 +
/////////////////////
 +
// LINK GENERATION //
 +
/////////////////////
 +
 +
// titledDiffLink --> titledWikiLink --> generalLink
 +
// wikiLink   --> titledWikiLink --> generalLink
 +
// editCounterLink --> generalLink
 +
 +
// TODO Make these functions return Element objects, not just raw HTML strings.
 +
 +
function titledDiffLink(l) {
 +
// article, text, title, from, to) {
 +
return titledWikiLink({
 +
article: l.article,
 +
action: l.to + '&oldid=' + l.from,
 +
newWin: l.newWin,
 +
noPopup: l.noPopup,
 +
text: l.text,
 +
title: l.title,
 +
/* hack: no oldid here */
 +
actionName: 'diff',
 +
});
 +
}
 +
 +
function wikiLink(l) {
 +
//{article:article, action:action, text:text, oldid, newid}) {
 +
if (
 +
!(typeof l.article == typeof {} && typeof l.action == typeof '' && typeof l.text == typeof '')
 +
)
 +
return null;
 +
if (typeof l.oldid == 'undefined') {
 +
l.oldid = null;
 +
}
 +
var savedOldid = l.oldid;
 +
if (!/^(edit|view|revert|render)$|^raw/.test(l.action)) {
 +
l.oldid = null;
 +
}
 +
var hint = popupString(l.action + 'Hint'); // revertHint etc etc etc
 +
var oldidData = [l.oldid, safeDecodeURI(l.article)];
 +
var revisionString = tprintf('revision %s of %s', oldidData);
 +
log('revisionString=' + revisionString);
 +
switch (l.action) {
 +
case 'edit&section=new':
 +
hint = popupString('newSectionHint');
 +
break;
 +
case 'edit&undo=':
 +
if (l.diff && l.diff != 'prev' && savedOldid) {
 +
l.action += l.diff + '&undoafter=' + savedOldid;
 +
} else if (savedOldid) {
 +
l.action += savedOldid;
 +
}
 +
hint = popupString('undoHint');
 +
break;
 +
case 'raw&ctype=text/css':
 +
hint = popupString('rawHint');
 +
break;
 +
case 'revert':
 +
var p = parseParams(pg.current.link.href);
 +
l.action =
 +
'edit&autoclick=wpSave&actoken=' +
 +
autoClickToken() +
 +
'&autoimpl=' +
 +
popupString('autoedit_version') +
 +
'&autosummary=' +
 +
revertSummary(l.oldid, p.diff);
 +
if (p.diff == 'prev') {
 +
l.action += '&direction=prev';
 +
revisionString = tprintf('the revision prior to revision %s of %s', oldidData);
 +
}
 +
if (getValueOf('popupRevertSummaryPrompt')) {
 +
l.action += '&autosummaryprompt=true';
 +
}
 +
if (getValueOf('popupMinorReverts')) {
 +
l.action += '&autominor=true';
 +
}
 +
log('revisionString is now ' + revisionString);
 +
break;
 +
case 'nullEdit':
 +
l.action =
 +
'edit&autoclick=wpSave&actoken=' +
 +
autoClickToken() +
 +
'&autoimpl=' +
 +
popupString('autoedit_version') +
 +
'&autosummary=null';
 +
break;
 +
case 'historyfeed':
 +
l.action = 'history&feed=rss';
 +
break;
 +
case 'markpatrolled':
 +
l.action = 'markpatrolled&rcid=' + l.rcid;
 +
}
 +
 +
if (hint) {
 +
if (l.oldid) {
 +
hint = simplePrintf(hint, [revisionString]);
 +
} else {
 +
hint = simplePrintf(hint, [safeDecodeURI(l.article)]);
 +
}
 +
} else {
 +
hint = safeDecodeURI(l.article + '&action=' + l.action) + l.oldid ? '&oldid=' + l.oldid : '';
 +
}
 +
 +
return titledWikiLink({
 +
article: l.article,
 +
action: l.action,
 +
text: l.text,
 +
newWin: l.newWin,
 +
title: hint,
 +
oldid: l.oldid,
 +
noPopup: l.noPopup,
 +
onclick: l.onclick,
 +
});
 +
}
 +
 +
function revertSummary(oldid, diff) {
 +
var ret = '';
 +
if (diff == 'prev') {
 +
ret = getValueOf('popupQueriedRevertToPreviousSummary');
 +
} else {
 +
ret = getValueOf('popupQueriedRevertSummary');
 +
}
 +
return ret + '&autorv=' + oldid;
 +
}
 +
 +
function titledWikiLink(l) {
 +
// possible properties of argument:
 +
// article, action, text, title, oldid, actionName, className, noPopup
 +
// oldid = null is fine here
 +
 +
// article and action are mandatory args
 +
 +
if (typeof l.article == 'undefined' || typeof l.action == 'undefined') {
 +
errlog('got undefined article or action in titledWikiLink');
 +
return null;
 +
}
 +
 +
var base = pg.wiki.titlebase + l.article.urlString();
 +
var url = base;
 +
 +
if (typeof l.actionName == 'undefined' || !l.actionName) {
 +
l.actionName = 'action';
 +
}
 +
 +
// no need to add &action=view, and this confuses anchors
 +
if (l.action != 'view') {
 +
url = base + '&' + l.actionName + '=' + l.action;
 +
}
 +
 +
if (typeof l.oldid != 'undefined' && l.oldid) {
 +
url += '&oldid=' + l.oldid;
 +
}
 +
 +
var cssClass = pg.misc.defaultNavlinkClassname;
 +
if (typeof l.className != 'undefined' && l.className) {
 +
cssClass = l.className;
 +
}
 +
 +
return generalNavLink({
 +
url: url,
 +
newWin: l.newWin,
 +
title: typeof l.title != 'undefined' ? l.title : null,
 +
text: typeof l.text != 'undefined' ? l.text : null,
 +
className: cssClass,
 +
noPopup: l.noPopup,
 +
onclick: l.onclick,
 +
});
 +
}
 +
 +
pg.fn.getLastContrib = function getLastContrib(wikipage, newWin) {
 +
getHistoryInfo(wikipage, function (x) {
 +
processLastContribInfo(x, { page: wikipage, newWin: newWin });
 +
});
 +
};
 +
 +
function processLastContribInfo(info, stuff) {
 +
if (!info.edits || !info.edits.length) {
 +
alert('Popups: an odd thing happened. Please retry.');
 +
return;
 +
}
 +
if (!info.firstNewEditor) {
 +
alert(
 +
tprintf('Only found one editor: %s made %s edits', [
 +
info.edits[0].editor,
 +
info.edits.length,
 +
])
 +
);
 +
return;
 +
}
 +
var newUrl =
 +
pg.wiki.titlebase +
 +
new Title(stuff.page).urlString() +
 +
'&diff=cur&oldid=' +
 +
info.firstNewEditor.oldid;
 +
displayUrl(newUrl, stuff.newWin);
 +
}
 +
 +
pg.fn.getDiffSinceMyEdit = function getDiffSinceMyEdit(wikipage, newWin) {
 +
getHistoryInfo(wikipage, function (x) {
 +
processDiffSinceMyEdit(x, { page: wikipage, newWin: newWin });
 +
});
 +
};
 +
 +
function processDiffSinceMyEdit(info, stuff) {
 +
if (!info.edits || !info.edits.length) {
 +
alert('Popups: something fishy happened. Please try again.');
 +
return;
 +
}
 +
var friendlyName = stuff.page.split('_').join(' ');
 +
if (!info.myLastEdit) {
 +
alert(
 +
tprintf("Couldn't find an edit by %s\nin the last %s edits to\n%s", [
 +
info.userName,
 +
getValueOf('popupHistoryLimit'),
 +
friendlyName,
 +
])
 +
);
 +
return;
 +
}
 +
if (info.myLastEdit.index === 0) {
 +
alert(
 +
tprintf('%s seems to be the last editor to the page %s', [info.userName, friendlyName])
 +
);
 +
return;
 +
}
 +
var newUrl =
 +
pg.wiki.titlebase +
 +
new Title(stuff.page).urlString() +
 +
'&diff=cur&oldid=' +
 +
info.myLastEdit.oldid;
 +
displayUrl(newUrl, stuff.newWin);
 +
}
 +
 +
function displayUrl(url, newWin) {
 +
if (newWin) {
 +
window.open(url);
 +
} else {
 +
document.location = url;
 +
}
 +
}
 +
 +
pg.fn.purgePopups = function purgePopups() {
 +
processAllPopups(true);
 +
setupCache(); // deletes all cached items (not browser cached, though...)
 +
pg.option = {};
 +
abortAllDownloads();
 +
};
 +
 +
function processAllPopups(nullify, banish) {
 +
for (var i = 0; pg.current.links && i < pg.current.links.length; ++i) {
 +
if (!pg.current.links[i].navpopup) {
 +
continue;
 +
}
 +
if (nullify || banish) pg.current.links[i].navpopup.banish();
 +
pg.current.links[i].simpleNoMore = false;
 +
if (nullify) pg.current.links[i].navpopup = null;
 +
}
 +
}
 +
 +
pg.fn.disablePopups = function disablePopups() {
 +
processAllPopups(false, true);
 +
setupTooltips(null, true);
 +
};
 +
 +
pg.fn.togglePreviews = function togglePreviews() {
 +
processAllPopups(true, true);
 +
pg.option.simplePopups = !pg.option.simplePopups;
 +
abortAllDownloads();
 +
};
 +
 +
function magicWatchLink(l) {
 +
//Yuck!! Would require a thorough redesign to add this as a click event though ...
 +
l.onclick = simplePrintf("pg.fn.modifyWatchlist('%s','%s');return false;", [
 +
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
 +
this.id,
 +
]);
 +
return wikiLink(l);
 +
}
 +
 +
pg.fn.modifyWatchlist = function modifyWatchlist(title, action) {
 +
var reqData = {
 +
action: 'watch',
 +
formatversion: 2,
 +
titles: title,
 +
uselang: mw.config.get('wgUserLanguage'),
 +
};
 +
if (action === 'unwatch') reqData.unwatch = true;
 +
 +
// Load the Addedwatchtext or Removedwatchtext message and show it
 +
var mwTitle = mw.Title.newFromText(title);
 +
var messageName;
 +
if (mwTitle && mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1) {
 +
messageName = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
 +
} else {
 +
messageName = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
 +
}
 +
$.when(
 +
getMwApi().postWithToken('watch', reqData),
 +
mw.loader.using(['mediawiki.api', 'mediawiki.jqueryMsg']).then(function () {
 +
return api.loadMessagesIfMissing([messageName]);
 +
})
 +
).done(function () {
 +
mw.notify(mw.message(messageName, title).parseDom());
 +
});
 +
};
 +
 +
function magicHistoryLink(l) {
 +
// FIXME use onclick change href trick to sort this out instead of window.open
 +
 +
var jsUrl = '',
 +
title = '',
 +
onClick = '';
 +
switch (l.id) {
 +
case 'lastContrib':
 +
onClick = simplePrintf("pg.fn.getLastContrib('%s',%s)", [
 +
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
 +
l.newWin,
 +
]);
 +
title = popupString('lastContribHint');
 +
break;
 +
case 'sinceMe':
 +
onClick = simplePrintf("pg.fn.getDiffSinceMyEdit('%s',%s)", [
 +
l.article.toString(true).split('\\').join('\\\\').split("'").join("\\'"),
 +
l.newWin,
 +
]);
 +
title = popupString('sinceMeHint');
 +
break;
 +
}
 +
jsUrl = 'javascript:' + onClick; // jshint ignore:line
 +
onClick += ';return false;';
 +
 +
return generalNavLink({
 +
url: jsUrl,
 +
newWin: false, // can't have new windows with JS links, I think
 +
title: title,
 +
text: l.text,
 +
noPopup: l.noPopup,
 +
onclick: onClick,
 +
});
 +
}
 +
 +
function popupMenuLink(l) {
 +
var jsUrl = simplePrintf('javascript:pg.fn.%s()', [l.id]); // jshint ignore:line
 +
var title = popupString(simplePrintf('%sHint', [l.id]));
 +
var onClick = simplePrintf('pg.fn.%s();return false;', [l.id]);
 +
return generalNavLink({
 +
url: jsUrl,
 +
newWin: false,
 +
title: title,
 +
text: l.text,
 +
noPopup: l.noPopup,
 +
onclick: onClick,
 +
});
 +
}
 +
 +
function specialLink(l) {
 +
// properties: article, specialpage, text, sep
 +
if (typeof l.specialpage == 'undefined' || !l.specialpage) return null;
 +
var base =
 +
pg.wiki.titlebase +
 +
mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +
 +
':' +
 +
l.specialpage;
 +
if (typeof l.sep == 'undefined' || l.sep === null) l.sep = '&target=';
 +
var article = l.article.urlString({
 +
keepSpaces: l.specialpage == 'Search',
 +
});
 +
var hint = popupString(l.specialpage + 'Hint');
 +
switch (l.specialpage) {
 +
case 'Log':
 +
switch (l.sep) {
 +
case '&user=':
 +
hint = popupString('userLogHint');
 +
break;
 +
case '&type=block&page=':
 +
hint = popupString('blockLogHint');
 +
break;
 +
case '&page=':
 +
hint = popupString('pageLogHint');
 +
break;
 +
case '&type=protect&page=':
 +
hint = popupString('protectLogHint');
 +
break;
 +
case '&type=delete&page=':
 +
hint = popupString('deleteLogHint');
 +
break;
 +
default:
 +
log('Unknown log type, sep=' + l.sep);
 +
hint = 'Missing hint (FIXME)';
 +
}
 +
break;
 +
case 'PrefixIndex':
 +
article += '/';
 +
break;
 +
}
 +
if (hint) hint = simplePrintf(hint, [safeDecodeURI(l.article)]);
 +
else hint = safeDecodeURI(l.specialpage + ':' + l.article);
 +
 +
var url = base + l.sep + article;
 +
return generalNavLink({
 +
url: url,
 +
title: hint,
 +
text: l.text,
 +
newWin: l.newWin,
 +
noPopup: l.noPopup,
 +
});
 +
}
 +
 +
function generalLink(l) {
 +
// l.url, l.text, l.title, l.newWin, l.className, l.noPopup, l.onclick
 +
if (typeof l.url == 'undefined') return null;
 +
 +
// only quotation marks in the url can screw us up now... I think
 +
var url = l.url.split('"').join('%22');
 +
 +
var ret = '<a href="' + url + '"';
 +
if (typeof l.title != 'undefined' && l.title) {
 +
ret += ' title="' + pg.escapeQuotesHTML(l.title) + '"';
 +
}
 +
if (typeof l.onclick != 'undefined' && l.onclick) {
 +
ret += ' onclick="' + pg.escapeQuotesHTML(l.onclick) + '"';
 +
}
 +
if (l.noPopup) {
 +
ret += ' noPopup=1';
 +
}
 +
var newWin;
 +
if (typeof l.newWin == 'undefined' || l.newWin === null) {
 +
newWin = getValueOf('popupNewWindows');
 +
} else {
 +
newWin = l.newWin;
 +
}
 +
if (newWin) {
 +
ret += ' target="_blank"';
 +
}
 +
if (typeof l.className != 'undefined' && l.className) {
 +
ret += ' class="' + l.className + '"';
 +
}
 +
ret += '>';
 +
if (typeof l.text == typeof '') {
 +
// We need to HTML-escape this to avoid XSS, but we also want to
 +
// display any existing HTML entities correctly, so unescape it first.
 +
// For example, the display text of the user page menu item is defined
 +
// as "user&nbsp;page", so we need to unescape first to avoid it being
 +
// escaped to "user&amp;nbsp;page".
 +
ret += pg.escapeQuotesHTML(pg.unescapeQuotesHTML(l.text));
 +
}
 +
ret += '</a>';
 +
return ret;
 +
}
 +
 +
function appendParamsToLink(linkstr, params) {
 +
var sp = linkstr.parenSplit(RegExp('(href="[^"]+?)"', 'i'));
 +
if (sp.length < 2) return null;
 +
var ret = sp.shift() + sp.shift();
 +
ret += '&' + params + '"';
 +
ret += sp.join('');
 +
return ret;
 +
}
 +
 +
function changeLinkTargetLink(x) {
 +
// newTarget, text, hint, summary, clickButton, minor, title (optional), alsoChangeLabel {
 +
if (x.newTarget) {
 +
log('changeLinkTargetLink: newTarget=' + x.newTarget);
 +
}
 +
if (x.oldTarget !== decodeURIComponent(x.oldTarget)) {
 +
log('This might be an input problem: ' + x.oldTarget);
 +
}
 +
 +
// FIXME: first character of page title as well as namespace should be case insensitive
 +
// eg [[:category:X1]] and [[:Category:X1]] are equivalent
 +
// this'll break if charAt(0) is nasty
 +
var cA = mw.util.escapeRegExp(x.oldTarget);
 +
var chs = cA.charAt(0).toUpperCase();
 +
chs = '[' + chs + chs.toLowerCase() + ']';
 +
var currentArticleRegexBit = chs + cA.substring(1);
 +
currentArticleRegexBit = currentArticleRegexBit
 +
.split(RegExp('(?:[_ ]+|%20)', 'g'))
 +
.join('(?:[_ ]+|%20)')
 +
.split('\\(')
 +
.join('(?:%28|\\()')
 +
.split('\\)')
 +
.join('(?:%29|\\))'); // why does this need to match encoded strings ? links in the document ?
 +
// leading and trailing space should be ignored, and anchor bits optional:
 +
currentArticleRegexBit = '\\s*(' + currentArticleRegexBit + '(?:#[^\\[\\|]*)?)\\s*';
 +
// e.g. Computer (archaic) -> \s*([Cc]omputer[_ ](?:%2528|\()archaic(?:%2528|\)))\s*
 +
 +
// autoedit=s~\[\[([Cc]ad)\]\]~[[Computer-aided%20design|$1]]~g;s~\[\[([Cc]AD)[|]~[[Computer-aided%20design|~g
 +
 +
var title = x.title || mw.config.get('wgPageName').split('_').join(' ');
 +
var lk = titledWikiLink({
 +
article: new Title(title),
 +
newWin: x.newWin,
 +
action: 'edit',
 +
text: x.text,
 +
title: x.hint,
 +
className: 'popup_change_title_link',
 +
});
 +
var cmd = '';
 +
if (x.newTarget) {
 +
// escape '&' and other nasties
 +
var t = x.newTarget;
 +
var s = mw.util.escapeRegExp(x.newTarget);
 +
if (x.alsoChangeLabel) {
 +
cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + ']]~g;';
 +
cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~g;';
 +
cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + ']]~g';
 +
} else {
 +
cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + '|$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
 +
var ret = '';
 +
 +
if (getValueOf('popupAppendRedirNavLinks') && getValueOf('popupNavLinks')) {
 +
ret += '<hr />';
 +
 +
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 (
 +
'<br> ' +
 +
popupString('Redirects') +
 +
popupString(' to ') +
 +
titledWikiLink({
 +
article: new Title().fromWikiText(redirMatch),
 +
action: 'view' /* FIXME: newWin */,
 +
text: safeDecodeURI(redirMatch),
 +
title: popupString('Bypass redirect'),
 +
})
 +
);
 +
}
 +
}
 +
 +
function arinLink(l) {
 +
if (!saneLinkCheck(l)) {
 +
return null;
 +
}
 +
if (!l.article.isIpUser() || !pg.wiki.wikimedia) return null;
 +
 +
var uN = l.article.userName();
 +
 +
return generalNavLink({
 +
url: 'http://ws.arin.net/cgi-bin/whois.pl?queryinput=' + encodeURIComponent(uN),
 +
newWin: l.newWin,
 +
title: tprintf('Look up %s in ARIN whois database', [uN]),
 +
text: l.text,
 +
noPopup: 1,
 +
});
 +
}
 +
 +
function toolDbName(cookieStyle) {
 +
var ret = mw.config.get('wgDBname');
 +
if (!cookieStyle) {
 +
ret += '_p';
 +
}
 +
return ret;
 +
}
 +
 +
function saneLinkCheck(l) {
 +
if (typeof l.article != typeof {} || typeof l.text != typeof '') {
 +
return false;
 +
}
 +
return true;
 +
}
 +
function editCounterLink(l) {
 +
if (!saneLinkCheck(l)) return null;
 +
if (!pg.wiki.wikimedia) return null;
 +
var uN = l.article.userName();
 +
var tool = getValueOf('popupEditCounterTool');
 +
var url;
 +
var defaultToolUrl = '//tools.wmflabs.org/supercount/index.php?user=$1&project=$2.$3';
 +
 +
switch (tool) {
 +
case 'custom':
 +
url = simplePrintf(getValueOf('popupEditCounterUrl'), [
 +
encodeURIComponent(uN),
 +
toolDbName(),
 +
]);
 +
break;
 +
case 'soxred': // no longer available
 +
case 'kate': // no longer available
 +
case 'interiot': // no longer available
 +
/* fall through */
 +
case 'supercount':
 +
default:
 +
var theWiki = pg.wiki.hostname.split('.');
 +
url = simplePrintf(defaultToolUrl, [encodeURIComponent(uN), theWiki[0], theWiki[1]]);
 +
}
 +
return generalNavLink({
 +
url: url,
 +
title: tprintf('editCounterLinkHint', [uN]),
 +
newWin: l.newWin,
 +
text: l.text,
 +
noPopup: 1,
 +
});
 +
}
 +
 +
function globalSearchLink(l) {
 +
if (!saneLinkCheck(l)) return null;
 +
 +
var base = 'http://vs.aka-online.de/cgi-bin/globalwpsearch.pl?timeout=120&search=';
 +
var article = l.article.urlString({ keepSpaces: true });
 +
 +
return generalNavLink({
 +
url: base + article,
 +
newWin: l.newWin,
 +
title: tprintf('globalSearchHint', [safeDecodeURI(l.article)]),
 +
text: l.text,
 +
noPopup: 1,
 +
});
 +
}
 +
 +
function googleLink(l) {
 +
if (!saneLinkCheck(l)) return null;
 +
 +
var base = 'https://www.google.com/search?q=';
 +
var article = l.article.urlString({ keepSpaces: true });
 +
 +
return generalNavLink({
 +
url: base + '%22' + article + '%22',
 +
newWin: l.newWin,
 +
title: tprintf('googleSearchHint', [safeDecodeURI(l.article)]),
 +
text: l.text,
 +
noPopup: 1,
 +
});
 +
}
 +
 +
function editorListLink(l) {
 +
if (!saneLinkCheck(l)) return null;
 +
var article = l.article.articleFromTalkPage() || l.article;
 +
var url =
 +
'https://xtools.wmflabs.org/articleinfo/' +
 +
encodeURI(pg.wiki.hostname) +
 +
'/' +
 +
article.urlString() +
 +
'?uselang=' +
 +
mw.config.get('wgUserLanguage');
 +
return generalNavLink({
 +
url: url,
 +
title: tprintf('editorListHint', [article]),
 +
newWin: l.newWin,
 +
text: l.text,
 +
noPopup: 1,
 +
});
 +
}
 +
 +
function generalNavLink(l) {
 +
l.className = l.className === null ? 'popupNavLink' : l.className;
 +
return generalLink(l);
 +
}
 +
 +
//////////////////////////////////////////////////
 +
// magic history links
 +
//
 +
 +
function getHistoryInfo(wikipage, whatNext) {
 +
log('getHistoryInfo');
 +
getHistory(
 +
wikipage,
 +
whatNext
 +
? function (d) {
 +
whatNext(processHistory(d));
 +
  }
 +
: processHistory
 +
);
 +
}
 +
 +
// FIXME eliminate pg.idNumber ... how? :-(
 +
 +
function getHistory(wikipage, onComplete) {
 +
log('getHistory');
 +
var url =
 +
pg.wiki.apiwikibase +
 +
'?format=json&formatversion=2&action=query&prop=revisions&titles=' +
 +
new Title(wikipage).urlString() +
 +
'&rvlimit=' +
 +
getValueOf('popupHistoryLimit');
 +
log('getHistory: url=' + url);
 +
return startDownload(url, pg.idNumber + 'history', onComplete);
 +
}
 +
 +
function processHistory(download) {
 +
var jsobj = getJsObj(download.data);
 +
try {
 +
var revisions = anyChild(jsobj.query.pages).revisions;
 +
var edits = [];
 +
for (var i = 0; i < revisions.length; ++i) {
 +
edits.push({ oldid: revisions[i].revid, editor: revisions[i].user });
 +
}
 +
log('processed ' + edits.length + ' edits');
 +
return finishProcessHistory(edits, mw.config.get('wgUserName'));
 +
} catch (someError) {
 +
log('Something went wrong with JSON business');
 +
return finishProcessHistory([]);
 +
}
 +
}
 +
 +
function finishProcessHistory(edits, userName) {
 +
var histInfo = {};
 +
 +
histInfo.edits = edits;
 +
histInfo.userName = userName;
 +
 +
for (var i = 0; i < edits.length; ++i) {
 +
if (typeof histInfo.myLastEdit === 'undefined' && userName && edits[i].editor == userName) {
 +
histInfo.myLastEdit = {
 +
index: i,
 +
oldid: edits[i].oldid,
 +
previd: i === 0 ? null : edits[i - 1].oldid,
 +
};
 +
}
 +
if (typeof histInfo.firstNewEditor === 'undefined' && edits[i].editor != edits[0].editor) {
 +
histInfo.firstNewEditor = {
 +
index: i,
 +
oldid: edits[i].oldid,
 +
previd: i === 0 ? null : edits[i - 1].oldid,
 +
};
 +
}
 +
}
 +
//pg.misc.historyInfo=histInfo;
 +
return histInfo;
 +
}
 +
//</NOLITE>
 +
// ENDFILE: links.js
 +
 +
// STARTFILE: options.js
 +
//////////////////////////////////////////////////
 +
// options
 +
 +
// check for existing value, else use default
 +
function defaultize(x) {
 +
if (pg.option[x] === null || typeof pg.option[x] == 'undefined') {
 +
if (typeof window[x] != 'undefined') pg.option[x] = window[x];
 +
else pg.option[x] = pg.optionDefault[x];
 +
}
 +
}
 +
 +
function newOption(x, def) {
 +
pg.optionDefault[x] = def;
 +
}
 +
 +
function setDefault(x, def) {
 +
return newOption(x, def);
 +
}
 +
 +
function getValueOf(varName) {
 +
defaultize(varName);
 +
return pg.option[varName];
 +
}
 +
 +
/*eslint-disable */
 +
function useDefaultOptions() {
 +
// for testing
 +
for (var p in pg.optionDefault) {
 +
pg.option[p] = pg.optionDefault[p];
 +
if (typeof window[p] != 'undefined') {
 +
delete window[p];
 +
}
 +
}
 +
}
 +
/*eslint-enable */
 +
 +
function setOptions() {
 +
// user-settable parameters and defaults
 +
var userIsSysop = false;
 +
if (mw.config.get('wgUserGroups')) {
 +
for (var g = 0; g < mw.config.get('wgUserGroups').length; ++g) {
 +
if (mw.config.get('wgUserGroups')[g] == 'sysop') userIsSysop = true;
 +
}
 +
}
 +
 +
// Basic options
 +
newOption('popupDelay', 0.5);
 +
newOption('popupHideDelay', 0.5);
 +
newOption('simplePopups', false);
 +
newOption('popupStructure', 'shortmenus'); // see later - default for popupStructure is 'original' if simplePopups is true
 +
newOption('popupActionsMenu', true);
 +
newOption('popupSetupMenu', true);
 +
newOption('popupAdminLinks', userIsSysop);
 +
newOption('popupShortcutKeys', false);
 +
newOption('popupHistoricalLinks', true);
 +
newOption('popupOnlyArticleLinks', true);
 +
newOption('removeTitles', true);
 +
newOption('popupMaxWidth', 350);
 +
newOption('popupSimplifyMainLink', true);
 +
newOption('popupAppendRedirNavLinks', true);
 +
newOption('popupTocLinks', false);
 +
newOption('popupSubpopups', true);
 +
newOption('popupDragHandle', false /* 'popupTopLinks'*/);
 +
newOption('popupLazyPreviews', true);
 +
newOption('popupLazyDownloads', true);
 +
newOption('popupAllDabsStubs', false);
 +
newOption('popupDebugging', false);
 +
newOption('popupActiveNavlinks', true);
 +
newOption('popupModifier', false); // ctrl, shift, alt or meta
 +
newOption('popupModifierAction', 'enable'); // or 'disable'
 +
newOption('popupDraggable', true);
 +
newOption('popupReview', false);
 +
newOption('popupLocale', false);
 +
newOption('popupDateTimeFormatterOptions', {
 +
year: 'numeric',
 +
month: 'long',
 +
day: 'numeric',
 +
hour12: false,
 +
hour: '2-digit',
 +
minute: '2-digit',
 +
second: '2-digit',
 +
});
 +
newOption('popupDateFormatterOptions', {
 +
year: 'numeric',
 +
month: 'long',
 +
day: 'numeric',
 +
});
 +
newOption('popupTimeFormatterOptions', {
 +
hour12: false,
 +
hour: '2-digit',
 +
minute: '2-digit',
 +
second: '2-digit',
 +
});
 +
 +
//<NOLITE>
 +
// images
 +
newOption('popupImages', true);
 +
newOption('imagePopupsForImages', true);
 +
newOption('popupNeverGetThumbs', false);
 +
//newOption('popupImagesToggleSize',      true);
 +
newOption('popupThumbAction', 'imagepage'); //'sizetoggle');
 +
newOption('popupImageSize', 60);
 +
newOption('popupImageSizeLarge', 200);
 +
 +
// redirs, dabs, reversion
 +
newOption('popupFixRedirs', false);
 +
newOption('popupRedirAutoClick', 'wpDiff');
 +
newOption('popupFixDabs', false);
 +
newOption('popupDabsAutoClick', 'wpDiff');
 +
newOption('popupRevertSummaryPrompt', false);
 +
newOption('popupMinorReverts', false);
 +
newOption('popupRedlinkRemoval', false);
 +
newOption('popupRedlinkAutoClick', 'wpDiff');
 +
newOption('popupWatchDisambiggedPages', null);
 +
newOption('popupWatchRedirredPages', null);
 +
newOption('popupDabWiktionary', 'last');
 +
 +
// navlinks
 +
newOption('popupNavLinks', true);
 +
newOption('popupNavLinkSeparator', ' &sdot; ');
 +
newOption('popupLastEditLink', true);
 +
newOption('popupEditCounterTool', 'supercount');
 +
newOption('popupEditCounterUrl', '');
 +
//</NOLITE>
 +
 +
// previews etc
 +
newOption('popupPreviews', true);
 +
newOption('popupSummaryData', true);
 +
newOption('popupMaxPreviewSentences', 5);
 +
newOption('popupMaxPreviewCharacters', 600);
 +
newOption('popupLastModified', true);
 +
newOption('popupPreviewKillTemplates', true);
 +
newOption('popupPreviewRawTemplates', true);
 +
newOption('popupPreviewFirstParOnly', true);
 +
newOption('popupPreviewCutHeadings', true);
 +
newOption('popupPreviewButton', false);
 +
newOption('popupPreviewButtonEvent', 'click');
 +
 +
//<NOLITE>
 +
// diffs
 +
newOption('popupPreviewDiffs', true);
 +
newOption('popupDiffMaxLines', 100);
 +
newOption('popupDiffContextLines', 2);
 +
newOption('popupDiffContextCharacters', 40);
 +
newOption('popupDiffDates', true);
 +
newOption('popupDiffDatePrinter', 'toLocaleString'); // no longer in use
 +
 +
// edit summaries. God, these are ugly.
 +
newOption('popupReviewedSummary', popupString('defaultpopupReviewedSummary'));
 +
newOption('popupFixDabsSummary', popupString('defaultpopupFixDabsSummary'));
 +
newOption('popupExtendedRevertSummary', popupString('defaultpopupExtendedRevertSummary'));
 +
newOption('popupRevertSummary', popupString('defaultpopupRevertSummary'));
 +
newOption('popupRevertToPreviousSummary', popupString('defaultpopupRevertToPreviousSummary'));
 +
newOption('popupQueriedRevertSummary', popupString('defaultpopupQueriedRevertSummary'));
 +
newOption(
 +
'popupQueriedRevertToPreviousSummary',
 +
popupString('defaultpopupQueriedRevertToPreviousSummary')
 +
);
 +
newOption('popupFixRedirsSummary', popupString('defaultpopupFixRedirsSummary'));
 +
newOption('popupRedlinkSummary', popupString('defaultpopupRedlinkSummary'));
 +
newOption('popupRmDabLinkSummary', popupString('defaultpopupRmDabLinkSummary'));
 +
//</NOLITE>
 +
// misc
 +
newOption('popupHistoryLimit', 50);
 +
//<NOLITE>
 +
newOption('popupFilters', [
 +
popupFilterStubDetect,
 +
popupFilterDisambigDetect,
 +
popupFilterPageSize,
 +
popupFilterCountLinks,
 +
popupFilterCountImages,
 +
popupFilterCountCategories,
 +
popupFilterLastModified,
 +
]);
 +
newOption('extraPopupFilters', []);
 +
newOption('popupOnEditSelection', 'cursor');
 +
newOption('popupPreviewHistory', true);
 +
newOption('popupImageLinks', true);
 +
newOption('popupCategoryMembers', true);
 +
newOption('popupUserInfo', true);
 +
newOption('popupHistoryPreviewLimit', 25);
 +
newOption('popupContribsPreviewLimit', 25);
 +
newOption('popupRevDelUrl', '//en.wikipedia.org/wiki/Wikipedia:Revision_deletion');
 +
newOption('popupShowGender', true);
 +
//</NOLITE>
 +
 +
// new windows
 +
newOption('popupNewWindows', false);
 +
newOption('popupLinksNewWindow', { lastContrib: true, sinceMe: true });
 +
 +
// regexps
 +
newOption(
 +
'popupDabRegexp',
 +
'\\{\\{\\s*(d(ab|isamb(ig(uation)?)?)|(((geo|hn|road?|school|number)dis)|[234][lc][acw]|(road|ship)index))\\s*(\\|[^}]*)?\\}\\}|is a .*disambiguation.*page'
 +
);
 +
newOption('popupAnchorRegexp', 'anchors?'); //how to identify an anchors template
 +
newOption('popupStubRegexp', '(sect)?stub[}][}]|This .*-related article is a .*stub');
 +
newOption(
 +
'popupImageVarsRegexp',
 +
'image|image_(?:file|skyline|name|flag|seal)|cover|badge|logo'
 +
);
 +
}
 +
// ENDFILE: options.js
 +
 +
// STARTFILE: strings.js
 +
//<NOLITE>
 +
//////////////////////////////////////////////////
 +
// Translatable strings
 +
//////////////////////////////////////////////////
 +
//
 +
// See instructions at
 +
// https://en.wikipedia.org/wiki/Wikipedia:Tools/Navigation_popups/Translation
 +
 +
pg.string = {
 +
/////////////////////////////////////
 +
// summary data, searching etc.
 +
/////////////////////////////////////
 +
article: 'article',
 +
category: 'category',
 +
categories: 'categories',
 +
image: 'image',
 +
images: 'images',
 +
stub: 'stub',
 +
'section stub': 'section stub',
 +
'Empty page': 'Empty page',
 +
kB: 'kB',
 +
bytes: 'bytes',
 +
day: 'day',
 +
days: 'days',
 +
hour: 'hour',
 +
hours: 'hours',
 +
minute: 'minute',
 +
minutes: 'minutes',
 +
second: 'second',
 +
seconds: 'seconds',
 +
week: 'week',
 +
weeks: 'weeks',
 +
search: 'search',
 +
SearchHint: 'Find English Wikipedia articles containing %s',
 +
web: 'web',
 +
global: 'global',
 +
globalSearchHint: 'Search across Wikipedias in different languages for %s',
 +
googleSearchHint: 'Google for %s',
 +
/////////////////////////////////////
 +
// article-related actions and info
 +
// (some actions also apply to user pages)
 +
/////////////////////////////////////
 +
actions: 'actions', ///// view articles and view talk
 +
popupsMenu: 'popups',
 +
togglePreviewsHint: 'Toggle preview generation in popups on this page',
 +
'enable previews': 'enable previews',
 +
'disable previews': 'disable previews',
 +
'toggle previews': 'toggle previews',
 +
'show preview': 'show preview',
 +
reset: 'reset',
 +
'more...': 'more...',
 +
disable: 'disable popups',
 +
disablePopupsHint: 'Disable popups on this page. Reload page to re-enable.',
 +
historyfeedHint: 'RSS feed of recent changes to this page',
 +
purgePopupsHint: 'Reset popups, clearing all cached popup data.',
 +
PopupsHint: 'Reset popups, clearing all cached popup data.',
 +
spacebar: 'space',
 +
view: 'view',
 +
'view article': 'view article',
 +
viewHint: 'Go to %s',
 +
talk: 'talk',
 +
'talk page': 'talk page',
 +
'this&nbsp;revision': 'this&nbsp;revision',
 +
'revision %s of %s': 'revision %s of %s',
 +
'Revision %s of %s': 'Revision %s of %s',
 +
'the revision prior to revision %s of %s': 'the revision prior to revision %s of %s',
 +
'Toggle image size': 'Click to toggle image size',
 +
del: 'del', ///// delete, protect, move
 +
delete: 'delete',
 +
deleteHint: 'Delete %s',
 +
undeleteShort: 'un',
 +
UndeleteHint: 'Show the deletion history for %s',
 +
protect: 'protect',
 +
protectHint: 'Restrict editing rights to %s',
 +
unprotectShort: 'un',
 +
unprotectHint: 'Allow %s to be edited by anyone again',
 +
'send thanks': 'send thanks',
 +
ThanksHint: 'Send a thank you notification to this user',
 +
move: 'move',
 +
'move page': 'move page',
 +
MovepageHint: 'Change the title of %s',
 +
edit: 'edit', ///// edit articles and talk
 +
'edit article': 'edit article',
 +
editHint: 'Change the content of %s',
 +
'edit talk': 'edit talk',
 +
new: 'new',
 +
'new topic': 'new topic',
 +
newSectionHint: 'Start a new section on %s',
 +
'null edit': 'null edit',
 +
nullEditHint: 'Submit an edit to %s, making no changes ',
 +
hist: 'hist', ///// history, diffs, editors, related
 +
history: 'history',
 +
historyHint: 'List the changes made to %s',
 +
last: 'prev', // For labelling the previous revision in history pages; the key is "last" for backwards compatibility
 +
lastEdit: 'lastEdit',
 +
'mark patrolled': 'mark patrolled',
 +
markpatrolledHint: 'Mark this edit as patrolled',
 +
'Could not marked this edit as patrolled': 'Could not marked this edit as patrolled',
 +
'show last edit': 'most recent edit',
 +
'Show the last edit': 'Show the effects of the most recent change',
 +
lastContrib: 'lastContrib',
 +
'last set of edits': 'latest edits',
 +
lastContribHint: 'Show the net effect of changes made by the last editor',
 +
cur: 'cur',
 +
diffCur: 'diffCur',
 +
'Show changes since revision %s': 'Show changes since revision %s',
 +
'%s old': '%s old', // as in 4 weeks old
 +
oldEdit: 'oldEdit',
 +
purge: 'purge',
 +
purgeHint: 'Demand a fresh copy of %s',
 +
raw: 'source',
 +
rawHint: 'Download the source of %s',
 +
render: 'simple',
 +
renderHint: 'Show a plain HTML version of %s',
 +
'Show the edit made to get revision': 'Show the edit made to get revision',
 +
sinceMe: 'sinceMe',
 +
'changes since mine': 'diff my edit',
 +
sinceMeHint: 'Show changes since my last edit',
 +
"Couldn't find an edit by %s\nin the last %s edits to\n%s":
 +
"Couldn't find an edit by %s\nin the last %s edits to\n%s",
 +
eds: 'eds',
 +
editors: 'editors',
 +
editorListHint: 'List the users who have edited %s',
 +
related: 'related',
 +
relatedChanges: 'relatedChanges',
 +
'related changes': 'related changes',
 +
RecentchangeslinkedHint: 'Show changes in articles related to %s',
 +
editOld: 'editOld', ///// edit old version, or revert
 +
rv: 'rv',
 +
revert: 'revert',
 +
revertHint: 'Revert to %s',
 +
defaultpopupReviewedSummary:
 +
'Accepted by reviewing the [[Special:diff/%s/%s|difference]] between this version and previously accepted version using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupRedlinkSummary:
 +
'Removing link to empty page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupFixDabsSummary:
 +
'Disambiguate [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupFixRedirsSummary:
 +
'Redirect bypass from [[%s]] to [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupExtendedRevertSummary:
 +
'Revert to revision dated %s by %s, oldid %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupRevertToPreviousSummary:
 +
'Revert to the revision prior to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupRevertSummary:
 +
'Revert to revision %s using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupQueriedRevertToPreviousSummary:
 +
'Revert to the revision prior to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupQueriedRevertSummary:
 +
'Revert to revision $1 dated $2 by $3 using [[:en:Wikipedia:Tools/Navigation_popups|popups]]',
 +
defaultpopupRmDabLinkSummary:
 +
'Remove link to dab page [[%s]] using [[:en:Wikipedia:Tools/Navigation_popups|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&nbsp;page': 'user&nbsp;page',
 +
'user talk': 'user talk',
 +
'edit user talk': 'edit user talk',
 +
'leave comment': 'leave comment',
 +
email: 'email',
 +
'email user': 'email user',
 +
EmailuserHint: 'Send an email to %s',
 +
space: 'space', // short form for userSpace link
 +
PrefixIndexHint: 'Show pages in the userspace of %s',
 +
count: 'count', ///// contributions, log
 +
'edit counter': 'edit counter',
 +
editCounterLinkHint: 'Count the contributions made by %s',
 +
contribs: 'contribs',
 +
contributions: 'contributions',
 +
deletedContribs: 'deleted contributions',
 +
DeletedcontributionsHint: 'List deleted edits made by %s',
 +
ContributionsHint: 'List the contributions made by %s',
 +
log: 'log',
 +
'user log': 'user log',
 +
userLogHint: "Show %s's user log",
 +
arin: 'ARIN lookup', ///// ARIN lookup, block user or IP
 +
'Look up %s in ARIN whois database': 'Look up %s in the ARIN whois database',
 +
unblockShort: 'un',
 +
block: 'block',
 +
'block user': 'block user',
 +
IpblocklistHint: 'Unblock %s',
 +
BlockipHint: 'Prevent %s from editing',
 +
'block log': 'block log',
 +
blockLogHint: 'Show the block log for %s',
 +
protectLogHint: 'Show the protection log for %s',
 +
pageLogHint: 'Show the page log for %s',
 +
deleteLogHint: 'Show the deletion log for %s',
 +
'Invalid %s %s': 'The option %s is invalid: %s',
 +
'No backlinks found': 'No backlinks found',
 +
' and more': ' and more',
 +
undo: 'undo',
 +
undoHint: 'undo this edit',
 +
'Download preview data': 'Download preview data',
 +
'Invalid or IP user': 'Invalid or IP user',
 +
'Not a registered username': 'Not a registered username',
 +
BLOCKED: 'BLOCKED',
 +
'Has blocks': 'Has blocks',
 +
' edits since: ': ' edits since: ',
 +
'last edit on ': 'last edit on ',
 +
'he/him': 'he/him',
 +
'she/her': 'she/her',
 +
/////////////////////////////////////
 +
// Autoediting
 +
/////////////////////////////////////
 +
'Enter a non-empty edit summary or press cancel to abort':
 +
'Enter a non-empty edit summary or press cancel to abort',
 +
'Failed to get revision information, please edit manually.\n\n':
 +
'Failed to get revision information, please edit manually.\n\n',
 +
'The %s button has been automatically clicked. Please wait for the next page to load.':
 +
'The %s button has been automatically clicked. Please wait for the next page to load.',
 +
'Could not find button %s. Please check the settings in your javascript file.':
 +
'Could not find button %s. Please check the settings in your javascript file.',
 +
/////////////////////////////////////
 +
// Popups setup
 +
/////////////////////////////////////
 +
'Open full-size image': 'Open full-size image',
 +
zxy: 'zxy',
 +
autoedit_version: 'np20140416',
 +
};
 +
 +
function popupString(str) {
 +
if (typeof popupStrings != 'undefined' && popupStrings && popupStrings[str]) {
 +
return popupStrings[str];
 +
}
 +
if (pg.string[str]) {
 +
return pg.string[str];
 +
}
 +
return str;
 +
}
 +
 +
function tprintf(str, subs) {
 +
if (typeof subs != typeof []) {
 +
subs = [subs];
 +
}
 +
return simplePrintf(popupString(str), subs);
 +
}
 +
 +
//</NOLITE>
 +
// ENDFILE: strings.js
 +
 +
// STARTFILE: run.js
 +
////////////////////////////////////////////////////////////////////
 +
// Run things
 +
////////////////////////////////////////////////////////////////////
 +
 +
// For some reason popups requires a fully loaded page jQuery.ready(...) causes problems for some.
 +
// The old addOnloadHook did something similar to the below
 +
if (document.readyState == 'complete') autoEdit();
 +
//will setup popups
 +
else $(window).on('load', autoEdit);
 +
 +
// Support for MediaWiki's live preview, VisualEditor's saves and Echo's flyout.
 +
(function () {
 +
var once = true;
 +
function dynamicContentHandler($content) {
 +
// Try to detect the hook fired on initial page load and disregard
 +
// it, we already hook to onload (possibly to different parts of
 +
// page - it's configurable) and running twice might be bad. Ugly…
 +
if ($content.attr('id') == 'mw-content-text') {
 +
if (once) {
 +
once = false;
 +
return;
 +
}
 +
}
 +
 +
function registerHooksForVisibleNavpops() {
 +
for (var i = 0; pg.current.links && i < pg.current.links.length; ++i) {
 +
var navpop = pg.current.links[i].navpopup;
 +
if (!navpop || !navpop.isVisible()) {
 +
continue;
 +
}
 +
 +
Navpopup.tracker.addHook(posCheckerHook(navpop));
 +
}
 +
}
 +
 +
function doIt() {
 +
registerHooksForVisibleNavpops();
 +
$content.each(function () {
 +
this.ranSetupTooltipsAlready = false;
 +
setupTooltips(this);
 +
});
 +
}
 +
 +
setupPopups(doIt);
 +
}
 +
 +
// This hook is also fired after page load.
 +
mw.hook('wikipage.content').add(dynamicContentHandler);
 +
 +
mw.hook('ext.echo.overlay.beforeShowingOverlay').add(function ($overlay) {
 +
dynamicContentHandler($overlay.find('.mw-echo-state'));
 +
});
 +
})();
 
});
 
});
 
// ENDFILE: run.js
 
// ENDFILE: run.js

Aktuelle Version vom 3. Dezember 2022, 13:39 Uhr

// 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 */
/* eslint no-mixed-spaces-and-tabs: 0, no-empty: 0 */

$(function () {
	//////////////////////////////////////////////////
	// Globals
	//

	// Trying to shove as many of these as possible into the pg (popup globals) object
	var 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) {
			//<NOLITE>
			// the main initial call
			if (
				getValueOf('popupOnEditSelection') &&
				document &&
				document.editform &&
				document.editform.wpTextbox1
			) {
				document.editform.wpTextbox1.onmouseup = doSelectionPopup;
			}
			//</NOLITE>
			// article/content is a structure-dependent thing
			container = defaultPopupsContainer();
		}

		if (!remove && !force && container.ranSetupTooltipsAlready) {
			return;
		}
		container.ranSetupTooltipsAlready = !remove;

		var anchors;
		anchors = container.getElementsByTagName('A');
		setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);
	}

	function defaultPopupsContainer() {
		if (getValueOf('popupOnlyArticleLinks')) {
			return (
				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));
		var finish = begin + howmany;
		var loopend = Math.min(finish, anchors.length);
		var j = loopend - begin;
		log(
			'setupTooltips: anchors.length=' +
				anchors.length +
				', begin=' +
				begin +
				', howmany=' +
				howmany +
				', loopend=' +
				loopend +
				', remove=' +
				remove
		);
		var doTooltip = remove ? removeTooltip : addTooltip;
		// try a faster (?) loop construct
		if (j > 0) {
			do {
				var 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(function () {
				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() {
		var toc = document.getElementById('toc');
		if (toc) {
			var tocLinks = toc.getElementsByTagName('A');
			var tocLen = tocLinks.length;
			for (var 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) {
		var popupMaxWidth = getValueOf('popupMaxWidth');

		if (typeof popupMaxWidth === 'number') {
			var setMaxWidth = function () {
				np.mainDiv.style.maxWidth = popupMaxWidth + 'px';
				np.maxWidth = popupMaxWidth;
			};
			np.addHook(setMaxWidth, 'unhide', 'before');
		}
		//<NOLITE>
		np.addHook(addPopupShortcuts, 'unhide', 'after');
		np.addHook(rmPopupShortcuts, 'hide', 'before');
		//</NOLITE>
	}

	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
			var action = getValueOf('popupModifierAction');
			var key = action == 'disable' ? 'keyup' : 'keydown';
			var 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.
	 * @returns {Element|boolean} The targeted element, or false if one can't be found.
	 */
	function footnoteTarget(a) {
		var aTitle = Title.fromAnchor(a);
		// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly
		var anch = aTitle.anchor;
		if (!/^(cite_note-|_note-|endnote)/.test(anch)) {
			return false;
		}

		var lTitle = Title.fromURL(location.href);
		if (lTitle.toString(true) !== aTitle.toString(true)) {
			return false;
		}

		var el = document.getElementById(anch);
		while (el && typeof el.nodeName === 'string') {
			var 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('<hr />' + x.innerHTML, 'popupPreview', navpop.idNumber);
	}

	function modifierPressed(evt) {
		var 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
		var 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');
		}

		var 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) {
				var d = document.createElement('div');
				d.className = 'popupPreviewButtonDiv';
				var 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')) {
			var dragHandle = getValueOf('popupDragHandle') || null;
			if (dragHandle && dragHandle != 'all') {
				dragHandle += a.navpopup.idNumber;
			}
			setTimeout(function () {
				a.navpopup.makeDraggable(dragHandle);
			}, 150);
		}

		//<NOLITE>
		if (getValueOf('popupRedlinkRemoval') && a.className == 'new') {
			setPopupHTML('<br>' + popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);
		}
		//</NOLITE>
	}

	function debugData(navpopup) {
		if (getValueOf('popupDebugging') && navpopup.idNumber) {
			setPopupHTML(
				'idNumber=' + navpopup.idNumber + ', pending=' + navpopup.pending,
				'popupError',
				navpopup.idNumber
			);
		}
	}

	function newNavpopup(a, article) {
		var 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) {
		var diff = null,
			history = null;
		var params = parseParams(a.href);
		var oldid = typeof params.oldid == 'undefined' ? null : params.oldid;
		//<NOLITE>
		if (shouldShow(a, 'popupPreviewDiffs')) {
			diff = params.diff;
		}
		if (shouldShow(a, 'popupPreviewHistory')) {
			history = params.action == 'history';
		}
		//</NOLITE>
		a.navpopup.pending = 0;
		var referenceElement = footnoteTarget(a);
		if (referenceElement) {
			footnotePreview(referenceElement, a.navpopup);
			//<NOLITE>
		} 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);
			//</NOLITE>
		} 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
		var 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;
		//<NOLITE>
		var warnRedir = redirLink(target, navpop.article);
		setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);
		//</NOLITE>
		navpop.article = target;
		fillEmptySpans({ redir: true, redirTarget: target, navpopup: navpop });
		return loadPreview(target, null, navpop);
	}

	function insertPreview(download) {
		if (!download.owner) {
			return;
		}

		var 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 {
			var id = download.owner.redir ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';
			download.owner.addHook(
				function () {
					insertPreviewNow(download);
					return true;
				},
				'unhide',
				'after',
				id
			);
		}
	}

	function insertPreviewNow(download) {
		if (!download.owner) {
			return;
		}
		var wikiText = download.data;
		var navpop = download.owner;
		var art = navpop.redirTarget || navpop.originalArticle;

		//<NOLITE>
		makeFixDabs(wikiText, navpop);
		if (getValueOf('popupSummaryData')) {
			getPageInfo(wikiText, download);
			setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);
		}

		var imagePage = '';
		if (art.namespaceId() == pg.nsImageId) {
			imagePage = art.toString();
		} else {
			imagePage = getValidImageFromWikiText(wikiText);
		}
		if (imagePage) {
			loadImage(Title.fromWikiText(imagePage), navpop);
		}
		//</NOLITE>

		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
				var h =
					'<hr /><span style="font-family: monospace;">' +
					download.data.entify().split('\\n').join('<br />\\n') +
					'</span>';
				setPopupHTML(h, 'popupPreview', navpop.idNumber);
			} else {
				var p = prepPreviewmaker(download.data, art, navpop);
				p.showPreview();
			}
		}
	}

	function prepPreviewmaker(data, article, navpop) {
		// deal with tricksy anchors
		var d = anchorize(data, article.anchorString());
		var urlBase = joinPath([pg.wiki.articlebase, article.urlString()]);
		var 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;
		}
		var anchRe = RegExp(
			'(?:=+\\s*' +
				literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') +
				'\\s*=+|\\{\\{\\s*' +
				getValueOf('popupAnchorRegexp') +
				'\\s*(?:\\|[^|}]*)*?\\s*' +
				literalizeRegex(anch) +
				'\\s*(?:\\|[^}]*)?}})'
		);
		var match = d.match(anchRe);
		if (match && match.length > 0 && match[0]) {
			return d.substring(d.indexOf(match[0]));
		}

		// now try to deal with == foo [[bar|baz]] boom == -> #foo_baz_boom
		var lines = d.split('\n');
		for (var i = 0; i < lines.length; ++i) {
			lines[i] = lines[i]
				.replace(RegExp('[[]{2}([^|\\]]*?[|])?(.*?)[\\]]{2}', 'g'), '$2')
				.replace(/'''([^'])/g, '$1')
				.replace(RegExp("''([^'])", 'g'), '$1');
			if (lines[i].match(anchRe)) {
				return d.split('\n').slice(i).join('\n').replace(RegExp('^[^=]*'), '');
			}
		}
		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
	/**
	@fileoverview
	The {@link Drag} object, which enables objects to be dragged around.

	<pre>
	*************************************************
	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.
	</pre>
	*/

	/**
	 * 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 <code>null</code>.
		 * @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 <code>null</code>.
		 * @type Function
		 */
		this.endHook = null;
	}

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

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

		o.root = oRoot ? oRoot : o;

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

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

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

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

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

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

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

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

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

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

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

	// STARTFILE: structures.js
	//<NOLITE>
	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('<b><<mainlink>></b>', x.article, x.params);
		}
		return '';
	};
	pg.structures.original.popupTopLinks = function (x) {
		log('defaultstructure.popupTopLinks');
		if (getValueOf('popupNavLinks')) {
			return navLinksHTML(x.article, x.hint, x.params);
		}
		return '';
	};
	pg.structures.original.popupImage = function (x) {
		log('original.popupImage, x.article=' + x.article + ', x.navpop.idNumber=' + x.navpop.idNumber);
		return imageHTML(x.article, x.navpop.idNumber);
	};
	pg.structures.original.popupRedirTitle = pg.structures.original.popupTitle;
	pg.structures.original.popupRedirTopLinks = pg.structures.original.popupTopLinks;

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

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

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

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

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

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

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

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

		var normal = '<<whatLinksHere|shortcut=l|links here>>*<<relatedChanges|shortcut=r|related>>';
		return navlinkStringToHTML(
			'<br>if(user){' + user + '*}if(admin){' + admin + 'if(user){<br>}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 <br> at the end and put one at the beginning
		return '<br>' + pg.structures.fancy.popupTopLinks(x).replace(RegExp('<br>$', 'i'), '');
	};
	pg.structures.fancy2.popupLayout = function () {
		// move toplinks to after the title
		return [
			'popupError',
			'popupImage',
			'popupTitle',
			'popupUserData',
			'popupData',
			'popupTopLinks',
			'popupOtherLinks',
			'popupRedir',
			[
				'popupWarnRedir',
				'popupRedirTopLinks',
				'popupRedirTitle',
				'popupRedirData',
				'popupRedirOtherLinks',
			],
			'popupMiscTools',
			['popupRedlink'],
			'popupPrePreviewSep',
			'popupPreview',
			'popupSecondPreview',
			'popupPreviewMore',
			'popupPostPreview',
			'popupFixDab',
		];
	};

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

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

		if (getValueOf('popupActionsMenu')) {
			s.push('<<mainlink>>*' + dropdiv + menuTitle('actions'));
		} else {
			s.push(dropdiv + '<<mainlink>>');
		}
		s.push('<menu>');
		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('<hr />' + watch + protectDelete);
		s.push(
			'<hr />' +
				'if(talk){<<article|shortcut=a|view article>><<editArticle|edit article>>}' +
				'else{<<talk|shortcut=t|talk page>><<editTalk|edit talk>>' +
				'<<newTalk|shortcut=+|new topic>>}</menu>' +
				enddiv
		);

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

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

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

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

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

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

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

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

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

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

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

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

		var from, to, flags, tmp;

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

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

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

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

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

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

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

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

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

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

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

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

	function autoClickToken() {
		return mw.user.sessionId();
	}

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

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

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

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

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

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

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

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

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

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

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

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

		/**
		 * Flag set on <code>abort</code>
		 * @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 <code>aborted</code> 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 <code>null</code> on failure.
	 * @return {Date}
	 */
	Downloader.prototype.getLastModifiedDate = function () {
		if (!this.http) {
			return null;
		}
		var lastmod = null;
		try {
			lastmod = this.http.getResponseHeader('Last-Modified');
		} catch (err) {}
		if (lastmod) {
			return new Date(lastmod);
		}
		return null;
	};

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

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

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

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

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

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

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

	var Insta = {};

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

			user: {},

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

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

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

		// options with default values or backreferences
		Insta.conf.user.name = Insta.conf.user.name || 'Wikipedian';
		Insta.conf.user.signature =
			'[[' +
			Insta.conf.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) {
		var ll = typeof wiki == 'string' ? wiki.replace(/\r/g, '').split(/\n/) : wiki, // lines of wikicode
			o = '', // output
			p = 0, // para flag
			r; // result of passing a regexp to compareLineStringOrReg()

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

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

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

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

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

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

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

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

		function parse_list() {
			var prev = '';

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

				sh();

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

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

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

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

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

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

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

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

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

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

				prev = l_match[1];
			}

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

		function parse_table() {
			endl(f('<table>', compareLineStringOrReg(/^\{\|( .*)$/) ? r[1] : ''));

			for (; remain(); )
				if (compareLineStringOrReg('|'))
					switch (charAtPoint(1)) {
						case '}':
							endl('</table>');
							return;
						case '-':
							endl(f('<tr>', compareLineStringOrReg(/\|-*(.*)/)[1]));
							break;
						default:
							parse_table_data();
					}
				else if (compareLineStringOrReg('!')) {
					parse_table_data();
				} else {
					sh();
				}
		}

		function parse_table_data() {
			var td_line, match_i;

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

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

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

			ps('>');

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

				ps(parse_inline_nowiki(td_line.shift()));

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

			var tc = 0,
				td = [];

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

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

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

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

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

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

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

				var w_match;

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

			return '';
			//</NOLITE>
		}

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

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

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

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

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

			//</NOLITE>
			return str;
		}

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

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

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

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

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

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

				sh();
			}

		return o;
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		h = splitted.join('?');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		markNopopupSpanLinks.done = true;
	}

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

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

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

	// check cache to see if page exists

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

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

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

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

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

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

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

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

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

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

	//<NOLITE>

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

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

	//</NOLITE>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	// ENDFILE: tools.js

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

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

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

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

		ret = rmDupesFromSortedList(ret.sort());

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

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

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

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

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

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

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

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

	// STARTFILE: htmloutput.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		removeModifierKeyHandler(a);

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

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

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

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

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

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

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

		this.setData();
	}

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

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

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

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

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

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

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

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

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

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

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

	/**
	 * @private
	 */
	Previewmaker.prototype.killTables = function () {
		// tables are bad, too
		// this can be slow, but it's an inprovement over a browser hang
		// torture test: [[Comparison_of_Intel_Central_Processing_Units]]
		this.kill('{|', /[|]}\s*/, '{|');
		this.kill(/<table.*?>/i, /<\/table.*?>/i, /<table.*?>/i);
		// remove lines starting with a pipe for the hell of it (?)
		this.data = this.data.replace(RegExp('^[|].*$', 'mg'), '');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.killImages = function () {
		var forbiddenNamespaceAliases = [];
		jQuery.each(mw.config.get('wgNamespaceIds'), function (_localizedNamespaceLc, _namespaceId) {
			if (_namespaceId != pg.nsImageId && _namespaceId != pg.nsCategoryId) return;
			forbiddenNamespaceAliases.push(_localizedNamespaceLc.split(' ').join('[ _]')); //todo: escape regexp fragments!
		});

		// images and categories are a nono
		this.kill(
			RegExp('[[][[]\\s*(' + forbiddenNamespaceAliases.join('|') + ')\\s*:', 'i'),
			/\]\]\s*/,
			'[',
			']'
		);
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.killHTML = function () {
		// kill <ref ...>...</ref>
		this.kill(/<ref\b[^/>]*?>/i, /<\/ref>/i);

		// let's also delete entire lines starting with <. it's worth a try.
		this.data = this.data.replace(RegExp('(^|\\n) *<.*', 'g'), '\n');

		// and those pesky html tags, but not <nowiki> or <blockquote>
		var splitted = this.data.parenSplit(/(<[\w\W]*?(?:>|$|(?=<)))/);
		var len = splitted.length;
		for (var i = 1; i < len; i = i + 2) {
			switch (splitted[i]) {
				case '<nowiki>':
				case '</nowiki>':
				case '<blockquote>':
				case '</blockquote>':
					break;
				default:
					splitted[i] = '';
			}
		}
		this.data = splitted.join('');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.killChunks = function () {
		// heuristics alert
		// chunks of italic text? you crazy, man?
		var italicChunkRegex = new RegExp(
			"((^|\\n)\\s*:*\\s*''[^']([^']|'''|'[^']){20}(.|\\n[^\\n])*''[.!?\\s]*\\n)+",
			'g'
		);
		// keep stuff separated, though, so stick in \n (fixes [[Union Jack]]?
		this.data = this.data.replace(italicChunkRegex, '\n');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.mopup = function () {
		// we simply *can't* be doing with horizontal rules right now
		this.data = this.data.replace(RegExp('^-{4,}', 'mg'), '');

		// no indented lines
		this.data = this.data.replace(RegExp('(^|\\n) *:[^\\n]*', 'g'), '');

		// replace __TOC__, __NOTOC__ and whatever else there is
		// this'll probably do
		this.data = this.data.replace(RegExp('^__[A-Z_]*__ *$', 'gmi'), '');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.firstBit = function () {
		// dont't be givin' me no subsequent paragraphs, you hear me?
		/// first we "normalize" section headings, removing whitespace after, adding before
		var d = this.data;

		if (getValueOf('popupPreviewCutHeadings')) {
			this.data = this.data.replace(RegExp('\\s*(==+[^=]*==+)\\s*', 'g'), '\n\n$1 ');
			/// then we want to get rid of paragraph breaks whose text ends badly
			this.data = this.data.replace(RegExp('([:;]) *\\n{2,}', 'g'), '$1\n');

			this.data = this.data.replace(RegExp('^[\\s\\n]*'), '');
			var stuff = RegExp('^([^\\n]|\\n[^\\n\\s])*').exec(this.data);
			if (stuff) {
				d = stuff[0];
			}
			if (!getValueOf('popupPreviewFirstParOnly')) {
				d = this.data;
			}

			/// now put \n\n after sections so that bullets and numbered lists work
			d = d.replace(RegExp('(==+[^=]*==+)\\s*', 'g'), '$1\n\n');
		}

		// Split sentences. Superfluous sentences are RIGHT OUT.
		// note: exactly 1 set of parens here needed to make the slice work
		d = d.parenSplit(RegExp('([!?.]+["' + "'" + ']*\\s)', 'g'));
		// leading space is bad, mmkay?
		d[0] = d[0].replace(RegExp('^\\s*'), '');

		var notSentenceEnds = RegExp(
			'([^.][a-z][.] *[a-z]|etc|sic|Dr|Mr|Mrs|Ms|St|no|op|cit|\\[[^\\]]*|\\s[A-Zvclm])$',
			'i'
		);
		d = this.fixSentenceEnds(d, notSentenceEnds);

		this.fullLength = d.join('').length;
		var n = this.maxSentences;
		var dd = this.firstSentences(d, n);

		do {
			dd = this.firstSentences(d, n);
			--n;
		} while (dd.length > this.maxCharacters && n !== 0);

		this.data = dd;
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.fixSentenceEnds = function (strs, reg) {
		// take an array of strings, strs
		// join strs[i] to strs[i+1] & strs[i+2] if strs[i] matches regex reg

		for (var i = 0; i < strs.length - 2; ++i) {
			if (reg.test(strs[i])) {
				var a = [];
				for (var j = 0; j < strs.length; ++j) {
					if (j < i) a[j] = strs[j];
					if (j == i) a[i] = strs[i] + strs[i + 1] + strs[i + 2];
					if (j > i + 2) a[j - 2] = strs[j];
				}
				return this.fixSentenceEnds(a, reg);
			}
		}
		return strs;
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.firstSentences = function (strs, howmany) {
		var t = strs.slice(0, 2 * howmany);
		return t.join('');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.killBadWhitespace = function () {
		// also cleans up isolated '''', eg [[Suntory Sungoliath]]
		this.data = this.data.replace(RegExp("^ *'+ *$", 'gm'), '');
	};

	/**
	 * Runs the various methods to generate the preview.
	 * The preview is stored in the <code>html</html> field.
	 * @private
	 */
	Previewmaker.prototype.makePreview = function () {
		if (
			this.owner.article.namespaceId() != pg.nsTemplateId &&
			this.owner.article.namespaceId() != pg.nsImageId
		) {
			this.killComments();
			this.killDivs();
			this.killGalleries();
			this.killBoxTemplates();

			if (getValueOf('popupPreviewKillTemplates')) {
				this.killTemplates();
			} else {
				this.killMultilineTemplates();
			}
			this.killTables();
			this.killImages();
			this.killHTML();
			this.killChunks();
			this.mopup();

			this.firstBit();
			this.killBadWhitespace();
		} else {
			this.killHTML();
		}
		this.html = wiki2html(this.data, this.baseUrl); // needs livepreview
		this.fixHTML();
		this.stripLongTemplates();
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.esWiki2HtmlPart = function (data) {
		var reLinks = /(?:\[\[([^|\]]*)(?:\|([^|\]]*))*]]([a-z]*))/gi; //match a wikilink
		reLinks.lastIndex = 0; //reset regex

		var match;
		var result = '';
		var postfixIndex = 0;
		while ((match = reLinks.exec(data))) {
			//match all wikilinks
			//FIXME: the way that link is built here isn't perfect. It is clickable, but popups preview won't recognize it in some cases.
			result +=
				pg.escapeQuotesHTML(data.substring(postfixIndex, match.index)) +
				'<a href="' +
				Insta.conf.paths.articles +
				pg.escapeQuotesHTML(match[1]) +
				'">' +
				pg.escapeQuotesHTML((match[2] ? match[2] : match[1]) + match[3]) +
				'</a>';
			postfixIndex = reLinks.lastIndex;
		}
		//append the rest
		result += pg.escapeQuotesHTML(data.substring(postfixIndex));

		return result;
	};
	Previewmaker.prototype.editSummaryPreview = function () {
		var reAes = /\/\* *(.*?) *\*\//g; //match the first section marker
		reAes.lastIndex = 0; //reset regex

		var match;

		match = reAes.exec(this.data);
		if (match) {
			//we have a section link. Split it, process it, combine it.
			var prefix = this.data.substring(0, match.index - 1);
			var section = match[1];
			var postfix = this.data.substring(reAes.lastIndex);

			var start = "<span class='autocomment'>";
			var end = '</span>';
			if (prefix.length > 0) start = this.esWiki2HtmlPart(prefix) + ' ' + start + '- ';
			if (postfix.length > 0) end = ': ' + end + this.esWiki2HtmlPart(postfix);

			var t = new Title().fromURL(this.baseUrl);
			t.anchorFromUtf(section);
			var sectionLink =
				Insta.conf.paths.articles +
				pg.escapeQuotesHTML(t.toString(true)) +
				'#' +
				pg.escapeQuotesHTML(t.anchor);
			return (
				start + '<a href="' + sectionLink + '">&rarr;</a> ' + pg.escapeQuotesHTML(section) + end
			);
		}

		//else there's no section link, htmlify the whole thing.
		return this.esWiki2HtmlPart(this.data);
	};

	//<NOLITE>
	/** Test function for debugging preview problems one step at a time. */
	/*eslint-disable */
	function previewSteps(txt) {
		try {
			txt = txt || document.editform.wpTextbox1.value;
		} catch (err) {
			if (pg.cache.pages.length > 0) {
				txt = pg.cache.pages[pg.cache.pages.length - 1].data;
			} else {
				alert('provide text or use an edit page');
			}
		}
		txt = txt.substring(0, 10000);
		var base = pg.wiki.articlebase + Title.fromURL(document.location.href).urlString();
		var p = new Previewmaker(txt, base, pg.current.link.navpopup);
		if (this.owner.article.namespaceId() != pg.nsTemplateId) {
			p.killComments();
			if (!confirm('done killComments(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killDivs();
			if (!confirm('done killDivs(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killGalleries();
			if (!confirm('done killGalleries(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killBoxTemplates();
			if (!confirm('done killBoxTemplates(). Continue?\n---\n' + p.data)) {
				return;
			}

			if (getValueOf('popupPreviewKillTemplates')) {
				p.killTemplates();
				if (!confirm('done killTemplates(). Continue?\n---\n' + p.data)) {
					return;
				}
			} else {
				p.killMultilineTemplates();
				if (!confirm('done killMultilineTemplates(). Continue?\n---\n' + p.data)) {
					return;
				}
			}

			p.killTables();
			if (!confirm('done killTables(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killImages();
			if (!confirm('done killImages(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killHTML();
			if (!confirm('done killHTML(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killChunks();
			if (!confirm('done killChunks(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.mopup();
			if (!confirm('done mopup(). Continue?\n---\n' + p.data)) {
				return;
			}

			p.firstBit();
			if (!confirm('done firstBit(). Continue?\n---\n' + p.data)) {
				return;
			}
			p.killBadWhitespace();
			if (!confirm('done killBadWhitespace(). Continue?\n---\n' + p.data)) {
				return;
			}
		}

		p.html = wiki2html(p.data, base); // needs livepreview
		p.fixHTML();
		if (!confirm('done fixHTML(). Continue?\n---\n' + p.html)) {
			return;
		}
		p.stripLongTemplates();
		if (!confirm('done stripLongTemplates(). Continue?\n---\n' + p.html)) {
			return;
		}
		alert('finished preview - end result follows.\n---\n' + p.html);
	}
	/*eslint-enable */
	//</NOLITE>

	/**
	 * Works around livepreview bugs.
	 * @private
	 */
	Previewmaker.prototype.fixHTML = function () {
		if (!this.html) return;

		var ret = this.html;

		// fix question marks in wiki links
		// maybe this'll break some stuff :-(
		ret = ret.replace(
			RegExp('(<a href="' + pg.wiki.articlePath + '/[^"]*)[?](.*?")', 'g'),
			'$1%3F$2'
		);
		ret = ret.replace(
			RegExp("(<a href='" + pg.wiki.articlePath + "/[^']*)[?](.*?')", 'g'),
			'$1%3F$2'
		);
		// FIXME fix up % too

		this.html = ret;
	};

	/**
	 * Generates the preview and displays it in the current popup.

	 * Does nothing if the generated preview is invalid or consists of whitespace only.
	 * Also activates wikilinks in the preview for subpopups if the popupSubpopups option is true.
	 */
	Previewmaker.prototype.showPreview = function () {
		this.makePreview();
		if (typeof this.html != typeof '') return;
		if (RegExp('^\\s*$').test(this.html)) return;
		setPopupHTML('<hr />', 'popupPrePreviewSep', this.owner.idNumber);
		setPopupTipsAndHTML(this.html, 'popupPreview', this.owner.idNumber, {
			owner: this.owner,
		});
		var more = this.fullLength > this.data.length ? this.moreLink() : '';
		setPopupHTML(more, 'popupPreviewMore', this.owner.idNumber);
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.moreLink = function () {
		var a = document.createElement('a');
		a.className = 'popupMoreLink';
		a.innerHTML = popupString('more...');
		var savedThis = this;
		a.onclick = function () {
			savedThis.maxCharacters += 2000;
			savedThis.maxSentences += 20;
			savedThis.setData();
			savedThis.showPreview();
		};
		return a;
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.stripLongTemplates = function () {
		// operates on the HTML!
		this.html = this.html.replace(
			RegExp('^.{0,1000}[{][{][^}]*?(<(p|br)( /)?>\\s*){2,}([^{}]*?[}][}])?', 'gi'),
			''
		);
		this.html = this.html.split('\n').join(' '); // workaround for <pre> templates
		this.html = this.html.replace(RegExp('[{][{][^}]*<pre>[^}]*[}][}]', 'gi'), '');
	};

	/**
	 * @private
	 */
	Previewmaker.prototype.killMultilineTemplates = function () {
		this.kill('{{{', '}}}');
		this.kill(RegExp('\\s*[{][{][^{}]*\\n'), '}}', '{{');
	};
	// ENDFILE: previewmaker.js

	// STARTFILE: querypreview.js
	function loadAPIPreview(queryType, article, navpop) {
		var art = new Title(article).urlString();
		var url = pg.wiki.apiwikibase + '?format=json&formatversion=2&action=query&';
		var htmlGenerator = function (/*a, d*/) {
			alert('invalid html generator');
		};
		var usernameart = '';
		switch (queryType) {
			case 'history':
				url +=
					'titles=' + art + '&prop=revisions&rvlimit=' + getValueOf('popupHistoryPreviewLimit');
				htmlGenerator = APIhistoryPreviewHTML;
				break;
			case 'category':
				url += 'list=categorymembers&cmtitle=' + art;
				htmlGenerator = APIcategoryPreviewHTML;
				break;
			case 'userinfo':
				var username = new Title(article).userName();
				usernameart = encodeURIComponent(username);
				if (pg.re.ipUser.test(username)) {
					url += 'list=blocks&bkprop=range|restrictions&bkip=' + usernameart;
				} else {
					url +=
						'list=users|usercontribs&usprop=blockinfo|groups|editcount|registration|gender&ususers=' +
						usernameart +
						'&meta=globaluserinfo&guiprop=groups|unattached&guiuser=' +
						usernameart +
						'&uclimit=1&ucprop=timestamp&ucuser=' +
						usernameart;
				}
				htmlGenerator = APIuserInfoPreviewHTML;
				break;
			case 'contribs':
				usernameart = encodeURIComponent(new Title(article).userName());
				url +=
					'list=usercontribs&ucuser=' +
					usernameart +
					'&uclimit=' +
					getValueOf('popupContribsPreviewLimit');
				htmlGenerator = APIcontribsPreviewHTML;
				break;
			case 'imagepagepreview':
				var trail = '';
				if (getValueOf('popupImageLinks')) {
					trail = '&list=imageusage&iutitle=' + art;
				}
				url += 'titles=' + art + '&prop=revisions|imageinfo&rvprop=content' + trail;
				htmlGenerator = APIimagepagePreviewHTML;
				break;
			case 'backlinks':
				url += 'list=backlinks&bltitle=' + art;
				htmlGenerator = APIbacklinksPreviewHTML;
				break;
			case 'revision':
				if (article.oldid) {
					url += 'revids=' + article.oldid;
				} else {
					url += 'titles=' + article.removeAnchor().urlString();
				}
				url +=
					'&prop=revisions|pageprops|info|images|categories&rvprop=ids|timestamp|flags|comment|user|content&cllimit=max&imlimit=max';
				htmlGenerator = APIrevisionPreviewHTML;
				break;
		}
		pendingNavpopTask(navpop);
		var callback = function (d) {
			log('callback of API functions was hit');
			if (queryType === 'userinfo') {
				// We need to do another API request
				fetchUserGroupNames(d.data).then(function () {
					showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
				});
				return;
			}
			showAPIPreview(queryType, htmlGenerator(article, d, navpop), navpop.idNumber, navpop, d);
		};
		var go = function () {
			getPageWithCaching(url, callback, navpop);
			return true;
		};

		if (navpop.visible || !getValueOf('popupLazyDownloads')) {
			go();
		} else {
			navpop.addHook(go, 'unhide', 'before', 'DOWNLOAD_' + queryType + '_QUERY_DATA');
		}
	}

	function linkList(list) {
		list.sort(function (x, y) {
			return x == y ? 0 : x < y ? -1 : 1;
		});
		var buf = [];
		for (var i = 0; i < list.length; ++i) {
			buf.push(
				wikiLink({
					article: new Title(list[i]),
					text: list[i].split(' ').join('&nbsp;'),
					action: 'view',
				})
			);
		}
		return buf.join(', ');
	}

	function getTimeOffset() {
		var tz = mw.user.options.get('timecorrection');

		if (tz) {
			if (tz.indexOf('|') > -1) {
				// New format
				return parseInt(tz.split('|')[1], 10);
			}
		}
		return 0;
	}

	function getTimeZone() {
		if (!pg.user.timeZone) {
			var tz = mw.user.options.get('timecorrection');
			pg.user.timeZone = 'UTC';

			if (tz) {
				var tzComponents = tz.split('|');
				if (tzComponents.length === 3 && tzComponents[0] === 'ZoneInfo') {
					pg.user.timeZone = tzComponents[2];
				} else {
					errlog('Unexpected timezone information: ' + tz);
				}
			}
		}
		return pg.user.timeZone;
	}

	/**
	 * Should we use an offset or can we use proper timezones
	 */
	function useTimeOffset() {
		if (typeof Intl.DateTimeFormat.prototype.formatToParts === 'undefined') {
			// IE 11
			return true;
		}
		var tz = mw.user.options.get('timecorrection');
		if (tz && tz.indexOf('ZoneInfo|') === -1) {
			// System| Default system time, default for users who didn't configure timezone
			// Offset| Manual defined offset by user
			return true;
		}
		return false;
	}

	/**
	 * Array of locales for the purpose of javascript locale based formatting
	 * Filters down to those supported by the browser. Empty [] === System's default locale
	 */
	function getLocales() {
		if (!pg.user.locales) {
			var userLanguage = document.querySelector('html').getAttribute('lang'); // make sure we have HTML locale
			if (getValueOf('popupLocale')) {
				userLanguage = getValueOf('popupLocale');
			} else if (userLanguage === 'en') {
				// en.wp tends to treat this as international english / unspecified
				// but we have more specific settings in user options
				if (getMWDateFormat() === 'mdy') {
					userLanguage = 'en-US';
				} else {
					userLanguage = 'en-GB';
				}
			}
			pg.user.locales = Intl.DateTimeFormat.supportedLocalesOf([userLanguage, navigator.language]);
		}
		return pg.user.locales;
	}

	/**
	 * Retrieve configured MW date format for this user
	 * These can be
	 * default
	 * dmy: time, dmy
	 * mdy: time, mdy
	 * ymd: time, ymd
	 * dmyt: dmy, time
	 * dmyts: dmy, time + seconds
	 * ISO 8601: YYYY-MM-DDThh:mm:ss (local time)
	 *
	 * This isn't too useful for us, as JS doesn't have formatters to match these private specifiers
	 */
	function getMWDateFormat() {
		return mw.user.options.get('date');
	}

	/**
	 * Creates a HTML table that's shown in the history and user-contribs popups.
	 * @param {Object[]} h - a list of revisions, returned from the API
	 * @param {boolean} reallyContribs - true only if we're displaying user contributions
	 */
	function editPreviewTable(article, h, reallyContribs) {
		var html = ['<table>'];
		var day = null;
		var curart = article;
		var page = null;

		var makeFirstColumnLinks;
		if (reallyContribs) {
			// We're showing user contributions, so make (diff | hist) links
			makeFirstColumnLinks = function (currentRevision) {
				var result = '(';
				result +=
					'<a href="' +
					pg.wiki.titlebase +
					new Title(currentRevision.title).urlString() +
					'&diff=prev' +
					'&oldid=' +
					currentRevision.revid +
					'">' +
					popupString('diff') +
					'</a>';
				result += '&nbsp;|&nbsp;';
				result +=
					'<a href="' +
					pg.wiki.titlebase +
					new Title(currentRevision.title).urlString() +
					'&action=history">' +
					popupString('hist') +
					'</a>';
				result += ')';
				return result;
			};
		} else {
			// It's a regular history page, so make (cur | last) links
			var firstRevid = h[0].revid;
			makeFirstColumnLinks = function (currentRevision) {
				var result = '(';
				result +=
					'<a href="' +
					pg.wiki.titlebase +
					new Title(curart).urlString() +
					'&diff=' +
					firstRevid +
					'&oldid=' +
					currentRevision.revid +
					'">' +
					popupString('cur') +
					'</a>';
				result += '&nbsp;|&nbsp;';
				result +=
					'<a href="' +
					pg.wiki.titlebase +
					new Title(curart).urlString() +
					'&diff=prev&oldid=' +
					currentRevision.revid +
					'">' +
					popupString('last') +
					'</a>';
				result += ')';
				return result;
			};
		}

		for (var i = 0; i < h.length; ++i) {
			if (reallyContribs) {
				page = h[i].title;
				curart = new Title(page);
			}
			var minor = h[i].minor ? '<b>m </b>' : '';
			var editDate = new Date(h[i].timestamp);
			var thisDay = formattedDate(editDate);
			var thisTime = formattedTime(editDate);
			if (thisDay == day) {
				thisDay = '';
			} else {
				day = thisDay;
			}
			if (thisDay) {
				html.push(
					'<tr><td colspan=3><span class="popup_history_date">' + thisDay + '</span></td></tr>'
				);
			}
			html.push('<tr class="popup_history_row_' + (i % 2 ? 'odd' : 'even') + '">');
			html.push('<td>' + makeFirstColumnLinks(h[i]) + '</td>');
			html.push(
				'<td>' +
					'<a href="' +
					pg.wiki.titlebase +
					new Title(curart).urlString() +
					'&oldid=' +
					h[i].revid +
					'">' +
					thisTime +
					'</a></td>'
			);
			var col3url = '',
				col3txt = '';
			if (!reallyContribs) {
				var user = h[i].user;
				if (!h[i].userhidden) {
					if (pg.re.ipUser.test(user)) {
						col3url =
							pg.wiki.titlebase +
							mw.config.get('wgFormattedNamespaces')[pg.nsSpecialId] +
							':Contributions&target=' +
							new Title(user).urlString();
					} else {
						col3url =
							pg.wiki.titlebase +
							mw.config.get('wgFormattedNamespaces')[pg.nsUserId] +
							':' +
							new Title(user).urlString();
					}
					col3txt = pg.escapeQuotesHTML(user);
				} else {
					col3url = getValueOf('popupRevDelUrl');
					col3txt = pg.escapeQuotesHTML(popupString('revdel'));
				}
			} else {
				col3url = pg.wiki.titlebase + curart.urlString();
				col3txt = pg.escapeQuotesHTML(page);
			}
			html.push(
				'<td>' +
					(reallyContribs ? minor : '') +
					'<a href="' +
					col3url +
					'">' +
					col3txt +
					'</a></td>'
			);
			var comment = '';
			var c = h[i].comment || h[i].content;
			if (c) {
				comment = new Previewmaker(c, new Title(curart).toUrl()).editSummaryPreview();
			} else if (h[i].commenthidden) {
				comment = popupString('revdel');
			}
			html.push('<td>' + (!reallyContribs ? minor : '') + comment + '</td>');
			html.push('</tr>');
			html = [html.join('')];
		}
		html.push('</table>');
		return html.join('');
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		ret = '<hr />' + ret.join(', ');
		return ret;
	}

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

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

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

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

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

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

	// STARTFILE: images.js

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

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

		var art = image.urlString();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	diffBugAlert.list = {};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		setupCache();

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

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

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

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

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

				// namespaces etc
				setNamespaces();
				setInterwiki();

				// regexps
				setRegexps();
				setRedirs();

				// other stuff
				setMisc();
				setupLivePreview();

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

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

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

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

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

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

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

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

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

	function navLinksHTML(article, hint, params) {
		//oldid, rcid) {
		var str = '<span class="popupNavLinks">' + defaultNavlinkSpec() + '</span>';
		// BAM
		return navlinkStringToHTML(str, article, params);
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

				addReviewLink(navpop, 'popupMiscTools');

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

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

					getPageWithCaching(url, doneDiff, navpop);

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

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

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

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

		insertDiff(navpop);
	}

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

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

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

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

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

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

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

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

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

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

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

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

	function diffDatesTable(navpop) {
		var html = '<table class="popup_diff_dates">';
		html += diffDatesTableRow(navpop.diffData.newRev.revision, tprintf('New revision'));
		html += diffDatesTableRow(navpop.diffData.oldRev.revision, tprintf('Old revision'));
		html += '</table>';
		return html;
	}
	function diffDatesTableRow(revision, label) {
		var txt = '';
		var lastModifiedDate = new Date(revision.timestamp);

		txt = formattedDateTime(lastModifiedDate);

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

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

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

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

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

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

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

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

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

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

		// article and action are mandatory args

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		var title = x.title || mw.config.get('wgPageName').split('_').join(' ');
		var lk = titledWikiLink({
			article: new Title(title),
			newWin: x.newWin,
			action: 'edit',
			text: x.text,
			title: x.hint,
			className: 'popup_change_title_link',
		});
		var cmd = '';
		if (x.newTarget) {
			// escape '&' and other nasties
			var t = x.newTarget;
			var s = mw.util.escapeRegExp(x.newTarget);
			if (x.alsoChangeLabel) {
				cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + ']]~g;';
				cmd += 's~\\[\\[' + currentArticleRegexBit + '[|]~[[' + t + '|~g;';
				cmd += 's~\\[\\[' + s + '\\|' + s + '\\]\\]~[[' + t + ']]~g';
			} else {
				cmd += 's~\\[\\[' + currentArticleRegexBit + '\\]\\]~[[' + t + '|$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
		var ret = '';

		if (getValueOf('popupAppendRedirNavLinks') && getValueOf('popupNavLinks')) {
			ret += '<hr />';

			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 (
				'<br> ' +
				popupString('Redirects') +
				popupString(' to ') +
				titledWikiLink({
					article: new Title().fromWikiText(redirMatch),
					action: 'view' /* FIXME: newWin */,
					text: safeDecodeURI(redirMatch),
					title: popupString('Bypass redirect'),
				})
			);
		}
	}

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

		var uN = l.article.userName();

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

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

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

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

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

		var base = 'http://vs.aka-online.de/cgi-bin/globalwpsearch.pl?timeout=120&search=';
		var article = l.article.urlString({ keepSpaces: true });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			setupPopups(doIt);
		}

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

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