MediaWiki:Gadget-popups.js
// STARTFILE: main.js
// **********************************************************************
// ** **
// ** changes to this file affect many users. **
// ** please discuss on the talk page before editing **
// ** **
// **********************************************************************
// ** **
// ** if you do edit this file, be sure that your editor recognizes it **
// ** as utf8, or the weird and wonderful characters in the namespaces **
// ** below will be completely broken. You can check with the show **
// ** changes button before submitting the edit. **
// ** test: مدیا מיוחד Мэдыя **
// ** **
// **********************************************************************
/* eslint-env browser */
/* global $, jQuery, mw, window */
// Fix later
/* global log, errlog, popupStrings, wikEdUseWikEd, WikEdUpdateFrame */
$(() => {
//////////////////////////////////////////////////
// Globals
//
// Trying to shove as many of these as possible into the pg (popup globals) object
const pg = {
api: {}, // MediaWiki API requests
re: {}, // regexps
ns: {}, // namespaces
string: {}, // translatable strings
wiki: {}, // local site info
user: {}, // current user info
misc: {}, // YUCK PHOOEY
option: {}, // options, see newOption etc
optionDefault: {}, // default option values
flag: {}, // misc flags
cache: {}, // page and image cache
structures: {}, // navlink structures
timer: {}, // all sorts of timers (too damn many)
counter: {}, // .. and all sorts of counters
current: {}, // state info
fn: {}, // functions
endoflist: null,
};
/* Bail if the gadget/script is being loaded twice */
/* An element with id "pg" would add a window.pg property, ignore such property */
if (window.pg && !(window.pg instanceof HTMLElement)) {
return;
}
/* Export to global context */
window.pg = pg;
/// Local Variables: ///
/// mode:c ///
/// End: ///
// ENDFILE: main.js
// STARTFILE: actions.js
function setupTooltips(container, remove, force, popData) {
log('setupTooltips, container=' + container + ', remove=' + remove);
if (!container) {
// the main initial call
if (
getValueOf('popupOnEditSelection') &&
document &&
document.editform &&
document.editform.wpTextbox1
) {
document.editform.wpTextbox1.onmouseup = doSelectionPopup;
}
// article/content is a structure-dependent thing
container = defaultPopupsContainer();
}
if (!remove && !force && container.ranSetupTooltipsAlready) {
return;
}
container.ranSetupTooltipsAlready = !remove;
let anchors;
anchors = container.getElementsByTagName('A');
setupTooltipsLoop(anchors, 0, 250, 100, remove, popData);
}
function defaultPopupsContainer() {
if (getValueOf('popupOnlyArticleLinks')) {
return (
document.querySelector('.skin-vector-2022 .vector-body') ||
document.getElementById('mw_content') ||
document.getElementById('content') ||
document.getElementById('article') ||
document
);
}
return document;
}
function setupTooltipsLoop(anchors, begin, howmany, sleep, remove, popData) {
log(simplePrintf('setupTooltipsLoop(%s,%s,%s,%s,%s)', arguments));
const finish = begin + howmany;
const loopend = Math.min(finish, anchors.length);
let j = loopend - begin;
log(
'setupTooltips: anchors.length=' +
anchors.length +
', begin=' +
begin +
', howmany=' +
howmany +
', loopend=' +
loopend +
', remove=' +
remove
);
const doTooltip = remove ? removeTooltip : addTooltip;
// try a faster (?) loop construct
if (j > 0) {
do {
const a = anchors[loopend - j];
if (typeof a === 'undefined' || !a || !a.href) {
log('got null anchor at index ' + loopend - j);
continue;
}
doTooltip(a, popData);
} while (--j);
}
if (finish < anchors.length) {
setTimeout(() => {
setupTooltipsLoop(anchors, finish, howmany, sleep, remove, popData);
}, sleep);
} else {
if (!remove && !getValueOf('popupTocLinks')) {
rmTocTooltips();
}
pg.flag.finishedLoading = true;
}
}
// eliminate popups from the TOC
// This also kills any onclick stuff that used to be going on in the toc
function rmTocTooltips() {
const toc = document.getElementById('toc');
if (toc) {
const tocLinks = toc.getElementsByTagName('A');
const tocLen = tocLinks.length;
for (let j = 0; j < tocLen; ++j) {
removeTooltip(tocLinks[j], true);
}
}
}
function addTooltip(a, popData) {
if (!isPopupLink(a)) {
return;
}
a.onmouseover = mouseOverWikiLink;
a.onmouseout = mouseOutWikiLink;
a.onmousedown = killPopup;
a.hasPopup = true;
a.popData = popData;
}
function removeTooltip(a) {
if (!a.hasPopup) {
return;
}
a.onmouseover = null;
a.onmouseout = null;
if (a.originalTitle) {
a.title = a.originalTitle;
}
a.hasPopup = false;
}
function removeTitle(a) {
if (!a.originalTitle) {
a.originalTitle = a.title;
}
a.title = '';
}
function restoreTitle(a) {
if (a.title || !a.originalTitle) {
return;
}
a.title = a.originalTitle;
}
function registerHooks(np) {
const popupMaxWidth = getValueOf('popupMaxWidth');
if (typeof popupMaxWidth === 'number') {
const setMaxWidth = function () {
np.mainDiv.style.maxWidth = popupMaxWidth + 'px';
np.maxWidth = popupMaxWidth;
};
np.addHook(setMaxWidth, 'unhide', 'before');
}
np.addHook(addPopupShortcuts, 'unhide', 'after');
np.addHook(rmPopupShortcuts, 'hide', 'before');
}
function removeModifierKeyHandler(a) {
//remove listeners for modifier key if any that were added in mouseOverWikiLink
document.removeEventListener('keydown', a.modifierKeyHandler, false);
document.removeEventListener('keyup', a.modifierKeyHandler, false);
}
function mouseOverWikiLink(evt) {
if (!evt && window.event) {
evt = window.event;
}
// if the modifier is needed, listen for it,
// we will remove the listener when we mouseout of this link or kill popup.
if (getValueOf('popupModifier')) {
// if popupModifierAction = enable, we should popup when the modifier is pressed
// if popupModifierAction = disable, we should popup unless the modifier is pressed
const action = getValueOf('popupModifierAction');
const key = action == 'disable' ? 'keyup' : 'keydown';
const a = this;
a.modifierKeyHandler = function (evt) {
mouseOverWikiLink2(a, evt);
};
document.addEventListener(key, a.modifierKeyHandler, false);
}
return mouseOverWikiLink2(this, evt);
}
/**
* Gets the references list item that the provided footnote link targets. This
* is typically a li element within the ol.references element inside the reflist.
*
* @param {Element} a - A footnote link.
* @return {Element|boolean} The targeted element, or false if one can't be found.
*/
function footnoteTarget(a) {
const aTitle = Title.fromAnchor(a);
// We want ".3A" rather than "%3A" or "?" here, so use the anchor property directly
const anch = aTitle.anchor;
if (!/^(cite_note-|_note-|endnote)/.test(anch)) {
return false;
}
const lTitle = Title.fromURL(location.href);
if (lTitle.toString(true) !== aTitle.toString(true)) {
return false;
}
let el = document.getElementById(anch);
while (el && typeof el.nodeName === 'string') {
const nt = el.nodeName.toLowerCase();
if (nt === 'li') {
return el;
} else if (nt === 'body') {
return false;
} else if (el.parentNode) {
el = el.parentNode;
} else {
return false;
}
}
return false;
}
function footnotePreview(x, navpop) {
setPopupHTML('
' + x.innerHTML, 'popupPreview', navpop.idNumber);
}
function modifierPressed(evt) {
const mod = getValueOf('popupModifier');
if (!mod) {
return false;
}
if (!evt && window.event) {
evt = window.event;
}
return evt && mod && evt[mod.toLowerCase() + 'Key'];
}
// Checks if the correct modifier pressed/unpressed if needed
function isCorrectModifier(a, evt) {
if (!getValueOf('popupModifier')) {
return true;
}
// if popupModifierAction = enable, we should popup when the modifier is pressed
// if popupModifierAction = disable, we should popup unless the modifier is pressed
const action = getValueOf('popupModifierAction');
return (
(action == 'enable' && modifierPressed(evt)) || (action == 'disable' && !modifierPressed(evt))
);
}
function mouseOverWikiLink2(a, evt) {
if (!isCorrectModifier(a, evt)) {
return;
}
if (getValueOf('removeTitles')) {
removeTitle(a);
}
if (a == pg.current.link && a.navpopup && a.navpopup.isVisible()) {
return;
}
pg.current.link = a;
if (getValueOf('simplePopups') && !pg.option.popupStructure) {
// reset *default value* of popupStructure
setDefault('popupStructure', 'original');
}
const article = new Title().fromAnchor(a);
// set global variable (ugh) to hold article (wikipage)
pg.current.article = article;
if (!a.navpopup) {
a.navpopup = newNavpopup(a, article);
pg.current.linksHash[a.href] = a.navpopup;
pg.current.links.push(a);
}
if (a.navpopup.pending === null || a.navpopup.pending !== 0) {
// either fresh popups or those with unfinshed business are redone from scratch
simplePopupContent(a, article);
}
a.navpopup.showSoonIfStable(a.navpopup.delay);
clearInterval(pg.timer.checkPopupPosition);
pg.timer.checkPopupPosition = setInterval(checkPopupPosition, 600);
if (getValueOf('simplePopups')) {
if (getValueOf('popupPreviewButton') && !a.simpleNoMore) {
const d = document.createElement('div');
d.className = 'popupPreviewButtonDiv';
const s = document.createElement('span');
d.appendChild(s);
s.className = 'popupPreviewButton';
s['on' + getValueOf('popupPreviewButtonEvent')] = function () {
a.simpleNoMore = true;
d.style.display = 'none';
nonsimplePopupContent(a, article);
};
s.innerHTML = popupString('show preview');
setPopupHTML(d, 'popupPreview', a.navpopup.idNumber);
}
}
if (a.navpopup.pending !== 0) {
nonsimplePopupContent(a, article);
}
}
// simplePopupContent: the content that do not require additional download
// (it is shown even when simplePopups is true)
function simplePopupContent(a, article) {
/* FIXME hack */ a.navpopup.hasPopupMenu = false;
a.navpopup.setInnerHTML(popupHTML(a));
fillEmptySpans({ navpopup: a.navpopup });
if (getValueOf('popupDraggable')) {
let dragHandle = getValueOf('popupDragHandle') || null;
if (dragHandle && dragHandle != 'all') {
dragHandle += a.navpopup.idNumber;
}
setTimeout(() => {
a.navpopup.makeDraggable(dragHandle);
}, 150);
}
if (getValueOf('popupRedlinkRemoval') && a.className == 'new') {
setPopupHTML('
' + popupRedlinkHTML(article), 'popupRedlink', a.navpopup.idNumber);
}
}
function debugData(navpopup) {
if (getValueOf('popupDebugging') && navpopup.idNumber) {
setPopupHTML(
'idNumber=' + navpopup.idNumber + ', pending=' + navpopup.pending,
'popupError',
navpopup.idNumber
);
}
}
function newNavpopup(a, article) {
const navpopup = new Navpopup();
navpopup.fuzz = 5;
navpopup.delay = getValueOf('popupDelay') * 1000;
// increment global counter now
navpopup.idNumber = ++pg.idNumber;
navpopup.parentAnchor = a;
navpopup.parentPopup = a.popData && a.popData.owner;
navpopup.article = article;
registerHooks(navpopup);
return navpopup;
}
// Should we show nonsimple context?
// If simplePopups is set to true, then we do not show nonsimple context,
// but if a bottom "show preview" was clicked we do show nonsimple context
function shouldShowNonSimple(a) {
return !getValueOf('simplePopups') || a.simpleNoMore;
}
// Should we show nonsimple context govern by the option (e.g. popupUserInfo)?
// If the user explicitly asked for nonsimple context by setting the option to true,
// then we show it even in nonsimple mode.
function shouldShow(a, option) {
if (shouldShowNonSimple(a)) {
return getValueOf(option);
} else {
return typeof window[option] != 'undefined' && window[option];
}
}
function nonsimplePopupContent(a, article) {
let diff = null,
history = null;
const params = parseParams(a.href);
const oldid = typeof params.oldid == 'undefined' ? null : params.oldid;
if (shouldShow(a, 'popupPreviewDiffs')) {
diff = params.diff;
}
if (shouldShow(a, 'popupPreviewHistory')) {
history = params.action == 'history';
}
a.navpopup.pending = 0;
const referenceElement = footnoteTarget(a);
if (referenceElement) {
footnotePreview(referenceElement, a.navpopup);
} else if (diff || diff === 0) {
loadDiff(article, oldid, diff, a.navpopup);
} else if (history) {
loadAPIPreview('history', article, a.navpopup);
} else if (shouldShowNonSimple(a) && pg.re.contribs.test(a.href)) {
loadAPIPreview('contribs', article, a.navpopup);
} else if (shouldShowNonSimple(a) && pg.re.backlinks.test(a.href)) {
loadAPIPreview('backlinks', article, a.navpopup);
} else if (
// FIXME should be able to get all preview combinations with options
article.namespaceId() == pg.nsImageId &&
(shouldShow(a, 'imagePopupsForImages') || !anchorContainsImage(a))
) {
loadAPIPreview('imagepagepreview', article, a.navpopup);
loadImage(article, a.navpopup);
} else {
if (article.namespaceId() == pg.nsCategoryId && shouldShow(a, 'popupCategoryMembers')) {
loadAPIPreview('category', article, a.navpopup);
} else if (
(article.namespaceId() == pg.nsUserId || article.namespaceId() == pg.nsUsertalkId) &&
shouldShow(a, 'popupUserInfo')
) {
loadAPIPreview('userinfo', article, a.navpopup);
}
if (shouldShowNonSimple(a)) {
startArticlePreview(article, oldid, a.navpopup);
}
}
}
function pendingNavpopTask(navpop) {
if (navpop && navpop.pending === null) {
navpop.pending = 0;
}
++navpop.pending;
debugData(navpop);
}
function completedNavpopTask(navpop) {
if (navpop && navpop.pending) {
--navpop.pending;
}
debugData(navpop);
}
function startArticlePreview(article, oldid, navpop) {
navpop.redir = 0;
loadPreview(article, oldid, navpop);
}
function loadPreview(article, oldid, navpop) {
if (!navpop.redir) {
navpop.originalArticle = article;
}
article.oldid = oldid;
loadAPIPreview('revision', article, navpop);
}
function loadPreviewFromRedir(redirMatch, navpop) {
// redirMatch is a regex match
const target = new Title().fromWikiText(redirMatch[2]);
// overwrite (or add) anchor from original target
// mediawiki does overwrite; eg User:Lupin/foo3#Done
if (navpop.article.anchor) {
target.anchor = navpop.article.anchor;
}
navpop.redir++;
navpop.redirTarget = target;
const warnRedir = redirLink(target, navpop.article);
setPopupHTML(warnRedir, 'popupWarnRedir', navpop.idNumber);
navpop.article = target;
fillEmptySpans({ redir: true, redirTarget: target, navpopup: navpop });
return loadPreview(target, null, navpop);
}
function insertPreview(download) {
if (!download.owner) {
return;
}
const redirMatch = pg.re.redirect.exec(download.data);
if (download.owner.redir === 0 && redirMatch) {
loadPreviewFromRedir(redirMatch, download.owner);
return;
}
if (download.owner.visible || !getValueOf('popupLazyPreviews')) {
insertPreviewNow(download);
} else {
const id = download.owner.redir ? 'PREVIEW_REDIR_HOOK' : 'PREVIEW_HOOK';
download.owner.addHook(
() => {
insertPreviewNow(download);
return true;
},
'unhide',
'after',
id
);
}
}
function insertPreviewNow(download) {
if (!download.owner) {
return;
}
const wikiText = download.data;
const navpop = download.owner;
const art = navpop.redirTarget || navpop.originalArticle;
makeFixDabs(wikiText, navpop);
if (getValueOf('popupSummaryData')) {
getPageInfo(wikiText, download);
setPopupTrailer(getPageInfo(wikiText, download), navpop.idNumber);
}
let imagePage = '';
if (art.namespaceId() == pg.nsImageId) {
imagePage = art.toString();
} else {
imagePage = getValidImageFromWikiText(wikiText);
}
if (imagePage) {
loadImage(Title.fromWikiText(imagePage), navpop);
}
if (getValueOf('popupPreviews')) {
insertArticlePreview(download, art, navpop);
}
}
function insertArticlePreview(download, art, navpop) {
if (download && typeof download.data == typeof '') {
if (art.namespaceId() == pg.nsTemplateId && getValueOf('popupPreviewRawTemplates')) {
// FIXME compare/consolidate with diff escaping code for wikitext
const h =
'
' +
download.data.entify().split('\\n').join('
\\n') +
'
';setPopupHTML(h, 'popupPreview', navpop.idNumber);
} else {
const p = prepPreviewmaker(download.data, art, navpop);
p.showPreview();
}
}
}
function prepPreviewmaker(data, article, navpop) {
// deal with tricksy anchors
const d = anchorize(data, article.anchorString());
const urlBase = joinPath([pg.wiki.articlebase, article.urlString()]);
const p = new Previewmaker(d, urlBase, navpop);
return p;
}
// Try to imitate the way mediawiki generates HTML anchors from section titles
function anchorize(d, anch) {
if (!anch) {
return d;
}
const anchRe = RegExp(
'(?:=+\\s*' +
literalizeRegex(anch).replace(/[_ ]/g, '[_ ]') +
'\\s*=+|\\{\\{\\s*' +
getValueOf('popupAnchorRegexp') +
'\\s*(?:\\|[^|}]*)*?\\s*' +
literalizeRegex(anch) +
'\\s*(?:\\|[^}]*)?}})'
);
const match = d.match(anchRe);
if (match && match.length > 0 && match[0]) {
return d.substring(d.indexOf(match[0]));
}
// now try to deal with == foo baz boom == -> #foo_baz_boom
const lines = d.split('\n');
for (let i = 0; i < lines.length; ++i) {
lines[i] = lines[i]
.replace(/\*?[|])?(.*?)[\]]{2}/g, '$2')
.replace(/'''([^'])/g, '$1')
.replace(/''([^'])/g, '$1');
if (lines[i].match(anchRe)) {
return d.split('\n').slice(i).join('\n').replace(/^[^=]*/, '');
}
}
return d;
}
function killPopup() {
removeModifierKeyHandler(this);
if (getValueOf('popupShortcutKeys')) {
rmPopupShortcuts();
}
if (!pg) {
return;
}
if (pg.current.link && pg.current.link.navpopup) {
pg.current.link.navpopup.banish();
}
pg.current.link = null;
abortAllDownloads();
if (pg.timer.checkPopupPosition) {
clearInterval(pg.timer.checkPopupPosition);
pg.timer.checkPopupPosition = null;
}
return true; // preserve default action
}
// ENDFILE: actions.js
// STARTFILE: domdrag.js
/**
* @file
* The {@link Drag} object, which enables objects to be dragged around.
*
*
* *************************************************
* dom-drag.js
* 09.25.2001
* www.youngpup.net
* **************************************************
* 10.28.2001 - fixed minor bug where events
* sometimes fired off the handle, not the root.
* *************************************************
* Pared down, some hooks added by User:Lupin
*
* Copyright Aaron Boodman.
* Saying stupid things daily since March 2001.
*
*/
/**
* Creates a new Drag object. This is used to make various DOM elements draggable.
*
* @constructor
*/
function Drag() {
/**
* Condition to determine whether or not to drag. This function should take one parameter,
* an Event. To disable this, set it to null
.
*
* @type {Function}
*/
this.startCondition = null;
/**
* Hook to be run when the drag finishes. This is passed the final coordinates of the
* dragged object (two integers, x and y). To disables this, set it to null
.
*
* @type {Function}
*/
this.endHook = null;
}
/**
* Gets an event in a cross-browser manner.
*
* @param {Event} e
* @private
*/
Drag.prototype.fixE = function (e) {
if (typeof e == 'undefined') {
e = window.event;
}
if (typeof e.layerX == 'undefined') {
e.layerX = e.offsetX;
}
if (typeof e.layerY == 'undefined') {
e.layerY = e.offsetY;
}
return e;
};
/**
* Initialises the Drag instance by telling it which object you want to be draggable, and what
* you want to drag it by.
*
* @param {HTMLElement} o The "handle" by which oRoot
is dragged.
* @param {HTMLElement} oRoot The object which moves when o
is dragged, or o
if omitted.
*/
Drag.prototype.init = function (o, oRoot) {
const dragObj = this;
this.obj = o;
o.onmousedown = function (e) {
dragObj.start.apply(dragObj, [e]);
};
o.dragging = false;
o.popups_draggable = true;
o.hmode = true;
o.vmode = true;
o.root = oRoot || o;
if (isNaN(parseInt(o.root.style.left, 10))) {
o.root.style.left = '0px';
}
if (isNaN(parseInt(o.root.style.top, 10))) {
o.root.style.top = '0px';
}
o.root.onthisStart = function () {};
o.root.onthisEnd = function () {};
o.root.onthis = function () {};
};
/**
* Starts the drag.
*
* @private
* @param {Event} e
*/
Drag.prototype.start = function (e) {
const o = this.obj; // = this;
e = this.fixE(e);
if (this.startCondition && !this.startCondition(e)) {
return;
}
const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
const x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
o.root.onthisStart(x, y);
o.lastMouseX = e.clientX;
o.lastMouseY = e.clientY;
const dragObj = this;
o.onmousemoveDefault = document.onmousemove;
o.dragging = true;
document.onmousemove = function (e) {
dragObj.drag.apply(dragObj, [e]);
};
document.onmouseup = function (e) {
dragObj.end.apply(dragObj, [e]);
};
return false;
};
/**
* Does the drag.
*
* @param {Event} e
* @private
*/
Drag.prototype.drag = function (e) {
e = this.fixE(e);
const o = this.obj;
const ey = e.clientY;
const ex = e.clientX;
const y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom, 10);
const x = parseInt(o.hmode ? o.root.style.left : o.root.style.right, 10);
let nx, ny;
nx = x + (ex - o.lastMouseX) * (o.hmode ? 1 : -1);
ny = y + (ey - o.lastMouseY) * (o.vmode ? 1 : -1);
this.obj.root.style[o.hmode ? 'left' : 'right'] = nx + 'px';
this.obj.root.style[o.vmode ? 'top' : 'bottom'] = ny + 'px';
this.obj.lastMouseX = ex;
this.obj.lastMouseY = ey;
this.obj.root.onthis(nx, ny);
return false;
};
/**
* Ends the drag.
*
* @private
*/
Drag.prototype.end = function () {
document.onmousemove = this.obj.onmousemoveDefault;
document.onmouseup = null;
this.obj.dragging = false;
if (this.endHook) {
this.endHook(
parseInt(this.obj.root.style[this.obj.hmode ? 'left' : 'right'], 10),
parseInt(this.obj.root.style[this.obj.vmode ? 'top' : 'bottom'], 10)
);
}
};
// ENDFILE: domdrag.js
// STARTFILE: structures.js
pg.structures.original = {};
pg.structures.original.popupLayout = function () {
return [
'popupError',
'popupImage',
'popupTopLinks',
'popupTitle',
'popupUserData',
'popupData',
'popupOtherLinks',
'popupRedir',
[
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
],
'popupMiscTools',
['popupRedlink'],
'popupPrePreviewSep',
'popupPreview',
'popupSecondPreview',
'popupPreviewMore',
'popupPostPreview',
'popupFixDab',
];
};
pg.structures.original.popupRedirSpans = function () {
return [
'popupRedir',
'popupWarnRedir',
'popupRedirTopLinks',
'popupRedirTitle',
'popupRedirData',
'popupRedirOtherLinks',
];
};
pg.structures.original.popupTitle = function (x) {
log('defaultstructure.popupTitle');
if (!getValueOf('popupNavLinks')) {
return navlinkStringToHTML('<
}
return '';
};
pg.structures.original.popupTopLinks = function (x) {
log('defaultstructure.popupTopLinks');
if (getValueOf('popupNavLinks')) {
return navLinksHTML(x.article, x.hint, x.params);
}
return '';
};
pg.structures.original.popupImage = function (x) {
log('original.popupImage, x.article=' + x.article + ', x.navpop.idNumber=' + x.navpop.idNumber);
return imageHTML(x.article, x.navpop.idNumber);
};
pg.structures.original.popupRedirTitle = pg.structures.original.popupTitle;
pg.structures.original.popupRedirTopLinks = pg.structures.original.popupTopLinks;
function copyStructure(oldStructure, newStructure) {
pg.structures[newStructure] = {};
for (const prop in pg.structures[oldStructure]) {
pg.structures[newStructure][prop] = pg.structures[oldStructure][prop];
}
}
copyStructure('original', 'nostalgia');
pg.structures.nostalgia.popupTopLinks = function (x) {
let str = '';
str += '<
// user links
// contribs - log - count - email - block
// count only if applicable; block only if popupAdminLinks
str += 'if(user){
<
str += 'if(wikimedia){*<
str += 'if(ipuser){}else{*<
// editing links
// talkpage -> edit|new - history - un|watch - article|edit
// other page -> edit - history - un|watch - talk|edit|new
const editstr = '<
const editOldidStr =
'if(oldid){<
editstr +
'}';
const historystr = '<
const watchstr = '<
str +=
'
if(talk){' +
editOldidStr +
'|<
'*' +
historystr +
'*' +
watchstr +
'*' +
'<
'}else{' + // not a talk page
editOldidStr +
'*' +
historystr +
'*' +
watchstr +
'*' +
'<
// misc links
str += '
<
str += 'if(admin){
}else{*}<
// admin links
str +=
'if(admin){*<
'<
return navlinkStringToHTML(str, x.article, x.params);
};
pg.structures.nostalgia.popupRedirTopLinks = pg.structures.nostalgia.popupTopLinks;
/** -- fancy -- */
copyStructure('original', 'fancy');
pg.structures.fancy.popupTitle = function (x) {
return navlinkStringToHTML('<
};
pg.structures.fancy.popupTopLinks = function (x) {
const hist =
'<
const watch = '<
const move = '<
return navlinkStringToHTML(
'if(talk){' +
'<
hist +
'*' +
'<
'*' +
watch +
'*' +
move +
'}else{<
hist +
'*<
'*' +
watch +
'*' +
move +
'}
',
x.article,
x.params
);
};
pg.structures.fancy.popupOtherLinks = function (x) {
const admin =
'<
let user = '<
user +=
'if(ipuser){|< popupString('email') + '>>}if(admin){*< const normal = '< return navlinkStringToHTML( ' 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 return ' }; pg.structures.fancy2.popupLayout = function () { // move toplinks to after the title return [ 'popupError', 'popupImage', 'popupTitle', 'popupUserData', 'popupData', 'popupTopLinks', 'popupOtherLinks', 'popupRedir', [ 'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ], 'popupMiscTools', ['popupRedlink'], 'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab', ]; }; /** -- menus -- */ copyStructure('original', 'menus'); pg.structures.menus.popupLayout = function () { return [ 'popupError', 'popupImage', 'popupTopLinks', 'popupTitle', 'popupOtherLinks', 'popupRedir', [ 'popupWarnRedir', 'popupRedirTopLinks', 'popupRedirTitle', 'popupRedirData', 'popupRedirOtherLinks', ], 'popupUserData', 'popupData', 'popupMiscTools', ['popupRedlink'], 'popupPrePreviewSep', 'popupPreview', 'popupSecondPreview', 'popupPreviewMore', 'popupPostPreview', 'popupFixDab', ]; }; pg.structures.menus.popupTopLinks = function (x, shorter) { // FIXME maybe this stuff should be cached const s = []; const dropclass = 'popup_drop'; const enddiv = '
if(user){' + user + '*}if(admin){' + admin + 'if(user){
}else{*}}' + normal,
at the end and put one at the beginning
' + pg.structures.fancy.popupTopLinks(x).replace(/
$/i, '');
let hist = '<
if (!shorter) {
hist = '
}
const lastedit = '<
const thank = 'if(diff){<
const jsHistory = '<
const linkshere = '<
const related = '<
const search =
'
'|<
const watch = '
const protect =
'
'<
const del =
'
const move = '<
const nullPurge = '
const viewOptions = '
const editRow =
'if(oldid){' +
'
'
'}else{<
const markPatrolled = 'if(rcid){<
const newTopic = 'if(talk){<
const protectDelete = 'if(admin){' + protect + del + '}';
if (getValueOf('popupActionsMenu')) {
s.push('<
} else {
s.push('
}
s.push('
' +enddiv
);
// user menu starts here
const email = '<
const contribs =
'if(wikimedia){
'if(admin){
s.push('if(user){*' + menuTitle(dropclass, 'user'));
s.push('
' + enddiv + '}');// popups menu starts here
if (getValueOf('popupSetupMenu') && !x.navpop.hasPopupMenu /* FIXME: hack */) {
x.navpop.hasPopupMenu = true;
s.push('*' + menuTitle(dropclass, 'popupsMenu') + '
' + enddiv);}
return navlinkStringToHTML(s.join(''), x.article, x.params);
};
function menuTitle(dropclass, s) {
const text = popupString(s); // i18n
const len = text.length;
return '
}
pg.structures.menus.popupRedirTitle = pg.structures.menus.popupTitle;
pg.structures.menus.popupRedirTopLinks = pg.structures.menus.popupTopLinks;
copyStructure('menus', 'shortmenus');
pg.structures.shortmenus.popupTopLinks = function (x) {
return pg.structures.menus.popupTopLinks(x, true);
};
pg.structures.shortmenus.popupRedirTopLinks = pg.structures.shortmenus.popupTopLinks;
pg.structures.lite = {};
pg.structures.lite.popupLayout = function () {
return ['popupTitle', 'popupPreview'];
};
pg.structures.lite.popupTitle = function (x) {
log(x.article + ': structures.lite.popupTitle');
//return navlinkStringToHTML('<
return '
};
// ENDFILE: structures.js
// STARTFILE: autoedit.js
function substitute(data, cmdBody) {
// alert('sub\nfrom: '+cmdBody.from+'\nto: '+cmdBody.to+'\nflags: '+cmdBody.flags);
const fromRe = RegExp(cmdBody.from, cmdBody.flags);
return data.replace(fromRe, cmdBody.to);
}
function execCmds(data, cmdList) {
for (let i = 0; i < cmdList.length; ++i) {
data = cmdList[i].action(data, cmdList[i]);
}
return data;
}
function parseCmd(str) {
// returns a list of commands
if (!str.length) {
return [];
}
let p = false;
switch (str.charAt(0)) {
case 's':
p = parseSubstitute(str);
break;
default:
return false;
}
if (p) {
return [p].concat(parseCmd(p.remainder));
}
return false;
}
// FIXME: Only used once here, confusing with native (and more widely-used) unescape, should probably be replaced
// Then again, unescape is semi-soft-deprecated, so we should look into replacing that too
function unEscape(str, sep) {
return str
.split('\\\\')
.join('\\')
.split('\\' + sep)
.join(sep)
.split('\\n')
.join('\n');
}
function parseSubstitute(str) {
// takes a string like s/a/b/flags;othercmds and parses it
let from, to, flags, tmp;
if (str.length < 4) {
return false;
}
const sep = str.charAt(1);
str = str.substring(2);
tmp = skipOver(str, sep);
if (tmp) {
from = tmp.segment;
str = tmp.remainder;
} else {
return false;
}
tmp = skipOver(str, sep);
if (tmp) {
to = tmp.segment;
str = tmp.remainder;
} else {
return false;
}
flags = '';
if (str.length) {
tmp = skipOver(str, ';') || skipToEnd(str, ';');
if (tmp) {
flags = tmp.segment;
str = tmp.remainder;
}
}
return {
action: substitute,
from: from,
to: to,
flags: flags,
remainder: str,
};
}
function skipOver(str, sep) {
const endSegment = findNext(str, sep);
if (endSegment < 0) {
return false;
}
const segment = unEscape(str.substring(0, endSegment), sep);
return { segment: segment, remainder: str.substring(endSegment + 1) };
}
/*eslint-disable*/
function skipToEnd(str, sep) {
return { segment: str, remainder: '' };
}
/*eslint-enable */
function findNext(str, ch) {
for (let i = 0; i < str.length; ++i) {
if (str.charAt(i) == '\\') {
i += 2;
}
if (str.charAt(i) == ch) {
return i;
}
}
return -1;
}
function setCheckbox(param, box) {
const val = mw.util.getParamValue(param);
if (val) {
switch (val) {
case '1':
case 'yes':
case 'true':
box.checked = true;
break;
case '0':
case 'no':
case 'false':
box.checked = false;
}
}
}
function autoEdit() {
setupPopups(() => {
if (mw.util.getParamValue('autoimpl') !== popupString('autoedit_version')) {
return false;
}
if (
mw.util.getParamValue('autowatchlist') &&
mw.util.getParamValue('actoken') === autoClickToken()
) {
pg.fn.modifyWatchlist(mw.util.getParamValue('title'), mw.util.getParamValue('action'));
}
if (!document.editform) {
return false;
}
if (autoEdit.alreadyRan) {
return false;
}
autoEdit.alreadyRan = true;
const cmdString = mw.util.getParamValue('autoedit');
if (cmdString) {
try {
const editbox = document.editform.wpTextbox1;
const cmdList = parseCmd(cmdString);
const input = editbox.value;
const output = execCmds(input, cmdList);
editbox.value = output;
} catch (dang) {
return;
}
// wikEd user script compatibility
if (typeof wikEdUseWikEd != 'undefined') {
if (wikEdUseWikEd === true) {
WikEdUpdateFrame();
}
}
}
setCheckbox('autominor', document.editform.wpMinoredit);
setCheckbox('autowatch', document.editform.wpWatchthis);
const rvid = mw.util.getParamValue('autorv');
if (rvid) {
const url =
pg.wiki.apiwikibase +
'?action=query&format=json&formatversion=2&prop=revisions&revids=' +
rvid;
startDownload(url, null, autoEdit2);
} else {
autoEdit2();
}
});
}
function autoEdit2(d) {
let summary = mw.util.getParamValue('autosummary');
let summaryprompt = mw.util.getParamValue('autosummaryprompt');
let summarynotice = '';
if (d && d.data && mw.util.getParamValue('autorv')) {
const s = getRvSummary(summary, d.data);
if (s === false) {
summaryprompt = true;
summarynotice = popupString(
'Failed to get revision information, please edit manually.\n\n'
);
summary = simplePrintf(summary, [
mw.util.getParamValue('autorv'),
'(unknown)',
'(unknown)',
]);
} else {
summary = s;
}
}
if (summaryprompt) {
const txt =
summarynotice + popupString('Enter a non-empty edit summary or press cancel to abort');
const response = prompt(txt, summary);
if (response) {
summary = response;
} else {
return;
}
}
if (summary) {
document.editform.wpSummary.value = summary;
}
// Attempt to avoid possible premature clicking of the save button
// (maybe delays in updates to the DOM are to blame?? or a red herring)
setTimeout(autoEdit3, 100);
}
function autoClickToken() {
return mw.user.sessionId();
}
function autoEdit3() {
if (mw.util.getParamValue('actoken') != autoClickToken()) {
return;
}
const btn = mw.util.getParamValue('autoclick');
if (btn) {
if (document.editform && document.editform[btn]) {
const button = document.editform[btn];
const msg = tprintf(
'The %s button has been automatically clicked. Please wait for the next page to load.',
[button.value]
);
bannerMessage(msg);
document.title = '(' + document.title + ')';
button.click();
} else {
alert(
tprintf('Could not find button %s. Please check the settings in your javascript file.', [
btn,
])
);
}
}
}
function bannerMessage(s) {
const headings = document.getElementsByTagName('h1');
if (headings) {
const div = document.createElement('div');
div.innerHTML = '' + pg.escapeQuotesHTML(s) + '';
headings[0].parentNode.insertBefore(div, headings[0]);
}
}
function getRvSummary(template, json) {
try {
const o = getJsObj(json);
const edit = anyChild(o.query.pages).revisions[0];
const timestamp = edit.timestamp
.split(/[A-Z]/g)
.join(' ')
.replace(/^ *| *$/g, '');
return simplePrintf(template, [
edit.revid,
timestamp,
edit.userhidden ? '(hidden)' : edit.user,
]);
} catch (badness) {
return false;
}
}
// ENDFILE: autoedit.js
// STARTFILE: downloader.js
/**
* @file
* {@link Downloader}, a xmlhttprequest wrapper, and helper functions.
*/
/**
* Creates a new Downloader
*
* @constructor
* @class The Downloader class. Create a new instance of this class to download stuff.
* @param {string} url The url to download. This can be omitted and supplied later.
*/
function Downloader(url) {
if (typeof XMLHttpRequest != 'undefined') {
this.http = new XMLHttpRequest();
}
/**
* The url to download
*
* @type {string}
*/
this.url = url;
/**
* A universally unique ID number
*
* @type {number}
*/
this.id = null;
/**
* Modification date, to be culled from the incoming headers
*
* @type {Date}
* @private
*/
this.lastModified = null;
/**
* What to do when the download completes successfully
*
* @type {Function}
* @private
*/
this.callbackFunction = null;
/**
* What to do on failure
*
* @type {Function}
* @private
*/
this.onFailure = null;
/**
* Flag set on abort
*
* @type {boolean}
*/
this.aborted = false;
/**
* HTTP method. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html for
* possibilities.
*
* @type {string}
*/
this.method = 'GET';
/**
* Async flag.
*
* @type {boolean}
*/
this.async = true;
}
new Downloader();
/** Submits the http request. */
Downloader.prototype.send = function (x) {
if (!this.http) {
return null;
}
return this.http.send(x);
};
/** Aborts the download, setting the aborted
field to true. */
Downloader.prototype.abort = function () {
if (!this.http) {
return null;
}
this.aborted = true;
return this.http.abort();
};
/** Returns the downloaded data. */
Downloader.prototype.getData = function () {
if (!this.http) {
return null;
}
return this.http.responseText;
};
/** Prepares the download. */
Downloader.prototype.setTarget = function () {
if (!this.http) {
return null;
}
this.http.open(this.method, this.url, this.async);
this.http.setRequestHeader('Api-User-Agent', pg.api.userAgent);
};
/** Gets the state of the download. */
Downloader.prototype.getReadyState = function () {
if (!this.http) {
return null;
}
return this.http.readyState;
};
pg.misc.downloadsInProgress = {};
/**
* Starts the download.
* Note that setTarget {@link Downloader#setTarget} must be run first
*/
Downloader.prototype.start = function () {
if (!this.http) {
return;
}
pg.misc.downloadsInProgress[this.id] = this;
this.http.send(null);
};
/**
* Gets the 'Last-Modified' date from the download headers.
* Should be run after the download completes.
* Returns null
on failure.
*
* @return {Date}
*/
Downloader.prototype.getLastModifiedDate = function () {
if (!this.http) {
return null;
}
let lastmod = null;
try {
lastmod = this.http.getResponseHeader('Last-Modified');
} catch (err) {}
if (lastmod) {
return new Date(lastmod);
}
return null;
};
/**
* Sets the callback function.
*
* @param {Function} f callback function, called as f(this)
on success
*/
Downloader.prototype.setCallback = function (f) {
if (!this.http) {
return;
}
this.http.onreadystatechange = f;
};
Downloader.prototype.getStatus = function () {
if (!this.http) {
return null;
}
return this.http.status;
};
//////////////////////////////////////////////////
// helper functions
/**
* Creates a new {@link Downloader} and prepares it for action.
*
* @param {string} url The url to download
* @param {number} id The ID of the {@link Downloader} object
* @param {Function} callback The callback function invoked on success
* @return {string|Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
*/
function newDownload(url, id, callback, onfailure) {
const d = new Downloader(url);
if (!d.http) {
return 'ohdear';
}
d.id = id;
d.setTarget();
if (!onfailure) {
onfailure = 2;
}
const f = function () {
if (d.getReadyState() == 4) {
delete pg.misc.downloadsInProgress[this.id];
try {
if (d.getStatus() == 200) {
d.data = d.getData();
d.lastModified = d.getLastModifiedDate();
callback(d);
} else if (typeof onfailure == typeof 1) {
if (onfailure > 0) {
// retry
newDownload(url, id, callback, onfailure - 1);
}
} else if (typeof onfailure === 'function') {
onfailure(d, url, id, callback);
}
} catch (somerr) {
/* ignore it */
}
}
};
d.setCallback(f);
return d;
}
/**
* Simulates a download from cached data.
* The supplied data is put into a {@link Downloader} as if it had downloaded it.
*
* @param {string} url The url.
* @param {number} id The ID.
* @param {Function} callback The callback, which is invoked immediately as callback(d)
,
* where d
is the new {@link Downloader}.
* @param {string} data The (cached) data.
* @param {Date} lastModified The (cached) last modified date.
*/
function fakeDownload(url, id, callback, data, lastModified, owner) {
const d = newDownload(url, callback);
d.owner = owner;
d.id = id;
d.data = data;
d.lastModified = lastModified;
return callback(d);
}
/**
* Starts a download.
*
* @param {string} url The url to download
* @param {number} id The ID of the {@link Downloader} object
* @param {Function} callback The callback function invoked on success
* @return {string|Downloader} the {@link Downloader} object created, or 'ohdear' if an unsupported browser
*/
function startDownload(url, id, callback) {
const d = newDownload(url, id, callback);
if (typeof d == typeof '') {
return d;
}
d.start();
return d;
}
/**
* Aborts all downloads which have been started.
*/
function abortAllDownloads() {
for (const x in pg.misc.downloadsInProgress) {
try {
pg.misc.downloadsInProgress[x].aborted = true;
pg.misc.downloadsInProgress[x].abort();
delete pg.misc.downloadsInProgress[x];
} catch (e) {}
}
}
// ENDFILE: downloader.js
// STARTFILE: livepreview.js
// TODO: location is often not correct (eg relative links in previews)
// NOTE: removed md5 and image and math parsing. was broken, lots of bytes.
/**
* InstaView - a Mediawiki to HTML converter in JavaScript
* Version 0.6.1
* Copyright (C) Pedro Fayolle 2005-2006
* https://en.wikipedia.org/wiki/User:Pilaf
* Distributed under the BSD license
*
* Changelog:
*
* 0.6.1
* - Fixed problem caused by \r characters
* - Improved inline formatting parser
*
* 0.6
* - Changed name to InstaView
* - Some major code reorganizations and factored out some common functions
* - Handled conversion of relative links (i.e. /foo)
* - Fixed misrendering of adjacent definition list items
* - Fixed bug in table headings handling
* - Changed date format in signatures to reflect Mediawiki's
* - Fixed handling of :Image:...
* - Updated MD5 function (hopefully it will work with UTF-8)
* - Fixed bug in handling of links inside images
*
* To do:
* - Better support for math tags
* - Full support for
* - Parser-based (as opposed to RegExp-based) inline wikicode handling (make it one-pass and
* bullet-proof)
* - Support for templates (through AJAX)
* - Support for coloured links (AJAX)
*/
const Insta = {};
function setupLivePreview() {
// options
Insta.conf = {
baseUrl: '',
user: {},
wiki: {
lang: pg.wiki.lang,
interwiki: pg.wiki.interwiki,
default_thumb_width: 180,
},
paths: {
articles: pg.wiki.articlePath + '/',
// Only used for Insta previews with images. (not in popups)
math: '/math/',
images: '//upload.wikimedia.org/wikipedia/en/', // FIXME getImageUrlStart(pg.wiki.hostname),
images_fallback: '//upload.wikimedia.org/wikipedia/commons/',
},
locale: {
user: mw.config.get('wgFormattedNamespaces')[pg.nsUserId],
image: mw.config.get('wgFormattedNamespaces')[pg.nsImageId],
category: mw.config.get('wgFormattedNamespaces')[pg.nsCategoryId],
// shouldn't be used in popup previews, i think
months: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
},
};
// options with default values or backreferences
Insta.conf.user.name = Insta.conf.user.name || 'Wikipedian';
Insta.conf.user.signature =
'[[' +
Insta.conf.locale.user +
':' +
Insta.conf.user.name +
'|' +
Insta.conf.user.name +
']]';
//Insta.conf.paths.images = '//upload.wikimedia.org/wikipedia/' + Insta.conf.wiki.lang + '/';
// define constants
Insta.BLOCK_IMAGE = new RegExp(
'^\\[\\[(?:File|Image|' +
Insta.conf.locale.image +
'):.*?\\|.*?(?:frame|thumbnail|thumb|none|right|left|center)',
'i'
);
}
Insta.dump = function (from, to) {
if (typeof from == 'string') {
from = document.getElementById(from);
}
if (typeof to == 'string') {
to = document.getElementById(to);
}
to.innerHTML = this.convert(from.value);
};
Insta.convert = function (wiki) {
let ll = typeof wiki == 'string' ? wiki.replace(/\r/g, '').split(/\n/) : wiki, // lines of wikicode
o = '', // output
p = 0, // para flag
r; // result of passing a regexp to compareLineStringOrReg()
// some shorthands
function remain() {
return ll.length;
}
function sh() {
return ll.shift();
} // shift
function ps(s) {
o += s;
} // push
// similar to C's printf, uses ? as placeholders, ?? to escape question marks
function f() {
let i = 1,
a = arguments,
f = a[0],
o = '',
c,
p;
for (; i < a.length; i++) {
if ((p = f.indexOf('?')) + 1) {
// allow character escaping
i -= c = f.charAt(p + 1) == '?' ? 1 : 0;
o += f.substring(0, p) + (c ? '?' : a[i]);
f = f.substr(p + 1 + c);
} else {
break;
}
}
return o + f;
}
function html_entities(s) {
return s.replace(/&/g, '&').replace(//g, '>');
}
// Wiki text parsing to html is a nightmare.
// The below functions deliberately don't escape the ampersand since this would make it more
// difficult, and we don't absolutely need to for how we need it. This means that any
// unescaped ampersands in wikitext will remain unescaped and can cause invalid HTML.
// Browsers should all be able to handle it though. We also escape significant wikimarkup
// characters to prevent further matching on the processed text.
function htmlescape_text(s) {
return s
.replace(/
.replace(/>/g, '>')
.replace(/:/g, ':')
.replace(/\[/g, '[')
.replace(/]/g, ']');
}
function htmlescape_attr(s) {
return htmlescape_text(s).replace(/'/g, ''').replace(/"/g, '"');
}
// return the first non matching character position between two strings
function str_imatch(a, b) {
for (var i = 0, l = Math.min(a.length, b.length); i < l; i++) {
if (a.charAt(i) != b.charAt(i)) {
break;
}
}
return i;
}
// compare current line against a string or regexp
// if passed a string it will compare only the first string.length characters
// if passed a regexp the result is stored in r
function compareLineStringOrReg(c) {
return typeof c == 'string' ?
ll[0] && ll[0].substr(0, c.length) == c :
(r = ll[0] && ll[0].match(c));
}
function compareLineString(c) {
return ll[0] == c;
} // compare current line against a string
function charAtPoint(p) {
return ll[0].charAt(p);
} // return char at pos p
function endl(s) {
ps(s);
sh();
}
function parse_list() {
let prev = '';
while (remain() && compareLineStringOrReg(/^([*#:;]+)(.*)$/)) {
const l_match = r;
sh();
const ipos = str_imatch(prev, l_match[1]);
// close uncontinued lists
for (let prevPos = prev.length - 1; prevPos >= ipos; prevPos--) {
const pi = prev.charAt(prevPos);
if (pi == '*') {
ps('');
} else if (pi == '#') {
ps('');
}
// close a dl only if the new item is not a dl item (:, ; or empty)
else if ($.inArray(l_match[1].charAt(prevPos), ['', '*', '#'])) {
ps('');
}
}
// open new lists
for (let matchPos = ipos; matchPos < l_match[1].length; matchPos++) {
const li = l_match[1].charAt(matchPos);
if (li == '*') {
ps('
- ');
- ' + parse_inline_nowiki(l_match[2]));
break;
case ';':
ps('
- ');
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('
- ' + parse_inline_nowiki(l_match[2]));
}
prev = l_match[1];
}
// close remaining lists
for (let i = prev.length - 1; i >= 0; i--) {
ps(f('?>', prev.charAt(i) == '*' ? 'ul' : prev.charAt(i) == '#' ? 'ol' : 'dl'));
}
}
function parse_table() {
endl(f('
', compareLineStringOrReg(/^\{\|( .*)$/) ? r[1] : ''));
');for (; remain(); ) {
if (compareLineStringOrReg('|')) {
switch (charAtPoint(1)) {
case '}':
endl('
return;
case '-':
endl(f('
', compareLineStringOrReg(/\|-*(.*)/)[1])); break;
default:
parse_table_data();
}
} else if (compareLineStringOrReg('!')) {
parse_table_data();
} else {
sh();
}
}
}
function parse_table_data() {
let td_line, match_i;
// 1: "|+", '|' or '+'
// 2: ??
// 3: attributes ??
// TODO: finish commenting this regexp
const td_match = sh().match(/^(\|\+|\||!)((?:([^[|]*?)\|(?!\|))?(.*))$/);
if (td_match[1] == '|+') {
ps('
} else {
ps('
}
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]));
}
let tc = 0,
td = [];
while (remain()) {
td.push(sh());
if (compareLineStringOrReg('|')) {
if (!tc) {
break;
}
// we're at the outer-most level (no nested tables), skip to td parse
else if (charAtPoint(1) == '}') {
tc--;
}
} else if (!tc && compareLineStringOrReg('!')) {
break;
} else if (compareLineStringOrReg('{|')) {
tc++;
}
}
if (td.length) {
ps(Insta.convert(td));
}
}
function parse_pre() {
ps('
');
do {
endl(parse_inline_nowiki(ll[0].substring(1)) + '\n');
} while (remain() && compareLineStringOrReg(' '));
ps('');
}
function parse_block_image() {
ps(parse_image(sh()));
}
function parse_image(str) {
// get what's in between "Image:" and ""
let tag = str.substring(str.indexOf(':') + 1, str.length - 2);
let width;
let attr = [],
filename,
caption = '';
let thumb = 0,
frame = 0,
center = 0;
let align = '';
if (tag.match(/\|/)) {
// manage nested links
let nesting = 0;
let last_attr;
for (let 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();
let 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 '';
}
function parse_inline_nowiki(str) {
let start,
lastend = 0;
let substart = 0,
nestlev = 0,
open,
close,
subloop;
let html = '';
while ((start = str.indexOf('
', substart)) != -1) { html += parse_inline_wiki(str.substring(lastend, start));
start += 8;
substart = start;
subloop = true;
do {
open = str.indexOf('
', substart); close = str.indexOf('', substart);
if (close <= open || open == -1) {
if (close == -1) {
return html + html_entities(str.substr(start));
}
substart = close + 9;
if (nestlev) {
nestlev--;
} else {
lastend = substart;
html += html_entities(str.substring(start, lastend - 9));
subloop = false;
}
} else {
substart = open + 8;
nestlev++;
}
} while (subloop);
}
return html + parse_inline_wiki(str.substr(lastend));
}
function parse_inline_images(str) {
let start,
substart = 0,
nestlev = 0;
let loop, close, open, wiki, html;
while ((start = str.indexOf('[[', substart)) != -1) {
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;
}
}
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) {
let italic,
bold,
i,
li,
o = '';
while ((i = str.indexOf("''", li)) + 1) {
o += str.substring(li, i);
li = i + 2;
if (str.charAt(i + 2) == "'") {
li++;
bold = !bold;
o += bold ? '' : '';
} else {
italic = !italic;
o += italic ? '' : '';
}
}
return o + str.substr(li);
}
function parse_inline_wiki(str) {
str = parse_inline_images(str);
// math
str = str.replace(/<(?:)math>(.*?)<\/math>/gi, '');
// Build a Mediawiki-formatted date string
let date = new Date();
let 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
str =
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'
),
($0, $1, $2) => f(
"?",
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'
),
''
)
.replace(
RegExp(
'\\[\\[:((?:' +
Insta.conf.locale.category +
'|Image|File|' +
Insta.conf.locale.image +
'|' +
Insta.conf.wiki.interwiki +
'):.*?)\\|([^\\]]+?)\\]\\](\\w*)',
'gi'
),
($0, $1, $2, $3) => f(
"?",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($2) + htmlescape_text($3)
)
)
.replace(/\[\[(\/[^|]*?)\]\]/g, ($0, $1) => f(
"?",
Insta.conf.baseUrl + htmlescape_attr($1),
htmlescape_text($1)
))
.replace(/\[\[(\/.*?)\|(.+?)\]\]/g, ($0, $1, $2) => f(
"?",
Insta.conf.baseUrl + htmlescape_attr($1),
htmlescape_text($2)
))
// Common links
.replace(/\[\[([^[|]*?)\]\](\w*)/g, ($0, $1, $2) => f(
"?",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($1) + htmlescape_text($2)
))
// Links
.replace(/\[\[([^[]*?)\|([^\]]+?)\]\](\w*)/g, ($0, $1, $2, $3) => f(
"?",
Insta.conf.paths.articles + htmlescape_attr($1),
htmlescape_text($2) + htmlescape_text($3)
))
// Namespace
.replace(/\[\[([^\]]*?:)?(.*?)( *\(.*?\))?\|\]\]/g, ($0, $1, $2, $3) => f(
"?",
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,
($0, $1, $2, $3, $4) => f(
"?",
htmlescape_attr($1),
htmlescape_attr($2) + htmlescape_attr($3),
htmlescape_text($4)
)
)
.replace(/\[http:\/\/(.*?)\]/g, ($0, $1) => f("[#]", htmlescape_attr($1)))
.replace(/\[(news|ftp|mailto|gopher|irc):(\/*)(.*?)\]/g, ($0, $1, $2, $3) => f(
"?:?",
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,
($0, $1, $2, $3, $4) => f(
"??:?",
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__', '');
return parse_inline_formatting(str);
}
// begin parsing
for (; remain(); ) {
if (compareLineStringOrReg(/^(={1,6})(.*)\1(.*)$/)) {
p = 0;
endl(f('
? ?', r[1].length, parse_inline_nowiki(r[2]), r[1].length, r[3]));} else if (compareLineStringOrReg(/^[*#:;]/)) {
p = 0;
parse_list();
} else if (compareLineStringOrReg(' ')) {
p = 0;
parse_pre();
} else if (compareLineStringOrReg('{|')) {
p = 0;
parse_table();
} else if (compareLineStringOrReg(/^----+$/)) {
p = 0;
endl('
');} else if (compareLineStringOrReg(Insta.BLOCK_IMAGE)) {
p = 0;
parse_block_image();
} else {
// handle paragraphs
if (compareLineString('')) {
p = remain() > 1 && ll[1] === '';
if (p) {
endl('
');}
} else {
if (!p) {
ps('
');
p = 1;
}
ps(parse_inline_nowiki(ll[0]) + ' ');
}
sh();
}
}
return o;
};
function wiki2html(txt, baseurl) {
Insta.conf.baseUrl = baseurl;
return Insta.convert(txt);
}
// ENDFILE: livepreview.js
// STARTFILE: pageinfo.js
function popupFilterPageSize(data) {
return formatBytes(data.length);
}
function popupFilterCountLinks(data) {
const num = countLinks(data);
return String(num) + ' ' + (num != 1 ? popupString('wikiLinks') : popupString('wikiLink'));
}
function popupFilterCountImages(data) {
const num = countImages(data);
return String(num) + ' ' + (num != 1 ? popupString('images') : popupString('image'));
}
function popupFilterCountCategories(data) {
const num = countCategories(data);
return (
String(num) + ' ' + (num != 1 ? popupString('categories') : popupString('category'))
);
}
function popupFilterLastModified(data, download) {
const lastmod = download.lastModified;
const now = new Date();
const age = now - lastmod;
if (lastmod && getValueOf('popupLastModified')) {
return tprintf('%s old', [formatAge(age)]).replace(/ /g, ' ');
}
return '';
}
function popupFilterWikibaseItem(data, download) {
return download.wikibaseItem ?
tprintf('%s', [
download.wikibaseRepo.replace(/\$1/g, download.wikibaseItem),
download.wikibaseItem,
]) :
'';
}
function formatAge(age) {
// coerce into a number
let a = 0 + age,
aa = a;
const seclen = 1000;
const minlen = 60 * seclen;
const hourlen = 60 * minlen;
const daylen = 24 * hourlen;
const weeklen = 7 * daylen;
const numweeks = (a - (a % weeklen)) / weeklen;
a = a - numweeks * weeklen;
const sweeks = addunit(numweeks, 'week');
const numdays = (a - (a % daylen)) / daylen;
a = a - numdays * daylen;
const sdays = addunit(numdays, 'day');
const numhours = (a - (a % hourlen)) / hourlen;
a = a - numhours * hourlen;
const shours = addunit(numhours, 'hour');
const nummins = (a - (a % minlen)) / minlen;
a = a - nummins * minlen;
const smins = addunit(nummins, 'minute');
const numsecs = (a - (a % seclen)) / seclen;
a = a - numsecs * seclen;
const 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 String(num) + ' ' + (num != 1 ? popupString(str + 's') : popupString(str));
}
function runPopupFilters(list, data, download) {
const ret = [];
for (let i = 0; i < list.length; ++i) {
if (list[i] && typeof list[i] == 'function') {
const 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');
}
const popupFilters = getValueOf('popupFilters') || [];
const extraPopupFilters = getValueOf('extraPopupFilters') || [];
const pageInfoArray = runPopupFilters(popupFilters.concat(extraPopupFilters), data, download);
let 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) {
const counts = stubCount(data, article);
if (counts.real) {
return popupString('stub');
}
if (counts.sect) {
return popupString('section stub');
}
return '';
}
function popupFilterDisambigDetect(data, download, article) {
if (!getValueOf('popupAllDabsStubs') && article.namespace()) {
return '';
}
return isDisambig(data, article) ? popupString('disambig') : '';
}
function formatBytes(num) {
return num > 949 ?
Math.round(num / 100) / 10 + popupString('kB') :
num + ' ' + popupString('bytes');
}
// ENDFILE: pageinfo.js
// STARTFILE: titles.js
/**
* @file Defines the {@link Title} class, and associated crufty functions.
*
*
Title
deals with article titles and their various* forms. {@link Stringwrapper} is the parent class of
*
Title
, 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 {number}
*/
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
Title
.*
* @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 '';
}
const split = this.anchor.parenSplit(/((?:[.][0-9A-F]{2})+)/);
const len = split.length;
let value;
for (let 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 () {
const split = this.anchor.parenSplit('/((?:[%][0-9A-F]{2})+)/');
const len = split.length;
for (let 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.
const splitted = h.split('?');
splitted[0] = splitted[0].split('&').join('%26');
h = splitted.join('?');
const contribs = pg.re.contribs.exec(h);
if (contribs) {
if (contribs[1] == 'title=') {
contribs[3] = contribs[3].split('+').join(' ');
}
const u = new Title(contribs[3]);
this.setUtf(
this.decodeNasties(
mw.config.get('wgFormattedNamespaces')[pg.nsUserId] + ':' + u.stripNamespace()
)
);
return this;
}
const 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;
}
const 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.
const 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
const m = pg.re.main.exec(h);
if (m === null) {
this.value = null;
} else {
const fromBotInterface = /[?](.+[&])?title=/.test(h);
if (fromBotInterface) {
m[2] = m[2].split('+').join('_');
}
const 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 {
let 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) {
const split = txt.parenSplit(/((?:[%][0-9A-Fa-f]{2})+)/);
const 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 (let 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);
};
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) {
const 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;
}
const namespaceId = this.namespaceId();
if (namespaceId >= 0 && namespaceId % 2 === 0) {
//non-special and subject namespace
const 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;
};
// Return canonical, localized namespace
Title.prototype.namespace = function () {
return mw.config.get('wgFormattedNamespaces')[this.namespaceId()];
};
Title.prototype.namespaceId = function () {
const n = this.value.indexOf(':');
if (n < 0) {
return 0;
} //mainspace
const namespaceId =
mw.config.get('wgNamespaceIds')[
this.value.substring(0, n).split(' ').join('_').toLowerCase()
];
if (typeof namespaceId == 'undefined') {
return 0;
} //mainspace
return namespaceId;
};
Title.prototype.talkPage = function () {
const 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;
}
const namespaceId = this.namespaceId();
if (namespaceId >= 0 && namespaceId % 2 == 1) {
//non-special and talk namespace
const 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 () {
const t = new Title(this.value);
t.toArticleFromTalkPage();
if (t.value) {
return t;
}
return null;
};
Title.prototype.articleFromTalkOrArticle = function () {
const t = new Title(this.value);
if (t.toArticleFromTalkPage()) {
return t;
}
return this;
};
Title.prototype.isIpUser = function () {
return pg.re.ipUser.test(this.userName());
};
Title.prototype.stripNamespace = function () {
// returns a string, not a Title
const n = this.value.indexOf(':');
if (n < 0) {
return this.value;
}
const 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;
}
const 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) {
const 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 = {};
}
let 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) {
const specialDiff = pg.re.specialdiff.exec(url);
if (specialDiff) {
const 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] };
}
}
const ret = {};
if (url.indexOf('?') == -1) {
return ret;
}
url = url.split('#')[0];
const s = url.split('?').slice(1).join();
const t = s.split('&');
for (let i = 0; i < t.length; ++i) {
const 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')) {
const 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) {
let ret;
// FIXME decodeURIComponent??
try {
ret = decodeURI(str.toString());
} catch (summat) {
return str;
}
for (let i = 0; i < pg.misc.decodeExtras.length; ++i) {
const from = pg.misc.decodeExtras[i].from;
const to = pg.misc.decodeExtras[i].to;
ret = ret.split(from).join(to);
}
return ret;
}
function safeDecodeURI(str) {
const ret = myDecodeURI(str);
return ret || str;
}
///////////
// TESTS //
///////////
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;
}
let sectStub = 0;
let realStub = 0;
if (pg.re.stub.test(data)) {
const s = data.parenSplit(pg.re.stub);
for (let 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;
}
const kids = a.childNodes;
for (let i = 0; i < kids.length; ++i) {
if (kids[i].nodeName == 'IMG') {
return true;
}
}
return false;
}
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;
}
const 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();
}
const s = $('.nopopups').toArray();
for (let i = 0; i < s.length; ++i) {
const as = s[i].getElementsByTagName('a');
for (let 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);
const i = findInPageCache(url);
let 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');
const callback = function (d) {
if (!d.aborted) {
addPageToCache(d);
onComplete(d);
}
};
return startDownload(url, owner.idNumber, callback);
}
function findInPageCache(url) {
for (let 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);
const 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);
let s = this;
let m = re.exec(s);
let ret = [];
while (m && s) {
// without the following loop, we have
// 'ab'.parenSplit(/a|(b)/) != 'ab'.split(/a|(b)/)
for (let 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) {
const s = re.toString();
let flags = '';
for (var j = s.length; s.charAt(j) != '/'; --j) {
if (s.charAt(j) != 'g') {
flags += s.charAt(j);
}
}
const 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
function getJsObj(json) {
try {
const json_ret = JSON.parse(json);
if (json_ret.warnings) {
for (let 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 (const p in obj) {
return obj[p];
}
return null;
}
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;
}
const len = arr.length;
for (let 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
const 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='';
return this.split('&')
.join('&')
.split('<')
.join('<')
.split('>')
.join('>' /*+shy*/)
.split('"')
.join('"');
};
// 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;
}
const ret = [];
const s = str.parenSplit(/(%s|\$[0-9]+)/);
let i = 0;
do {
ret.push(s.shift());
if (!s.length) {
break;
}
const cmd = s.shift();
if (cmd == '%s') {
if (i < subs.length) {
ret.push(subs[i]);
} else {
ret.push(cmd);
}
++i;
} else {
const 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) {
let ret = '';
for (let i = 0; i < mult; ++i) {
ret += s;
}
return ret;
}
function zeroFill(s, min) {
min = min || 2;
const 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) {
const ret = [];
for (let i = 0; i < o.length; ++i) {
ret.push(f(o[i]));
}
return ret;
}
function map_object(f, o) {
const ret = {};
for (const i in o) {
ret[o] = f(o[i]);
}
return ret;
}
pg.escapeQuotesHTML = function (text) {
return text
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/
.replace(/>/g, '>');
};
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.
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
};
// ENDFILE: tools.js
// STARTFILE: dab.js
//////////////////////////////////////////////////
// Dab-fixing code
//
function retargetDab(newTarget, oldTarget, friendlyCurrentArticleName, titleToEdit) {
log('retargetDab: newTarget=' + newTarget + ' oldTarget=' + oldTarget);
return changeLinkTargetLink({
newTarget: newTarget,
text: newTarget.split(' ').join(' '),
hint: tprintf('disambigHint', [newTarget]),
summary: simplePrintf(getValueOf('popupFixDabsSummary'), [
friendlyCurrentArticleName,
newTarget,
]),
clickButton: getValueOf('popupDabsAutoClick'),
minor: true,
oldTarget: oldTarget,
watch: getValueOf('popupWatchDisambiggedPages'),
title: titleToEdit,
});
}
function listLinks(wikitext, oldTarget, titleToEdit) {
// mediawiki strips trailing spaces, so we do the same
// testcase: https://en.wikipedia.org/w/index.php?title=Radial&oldid=97365633
const reg = /\[\[([^|]*?) *(\||\]\])/gi;
let ret = [];
const splitted = wikitext.parenSplit(reg);
// ^[a-z]+ should match interwiki links, hopefully (case-insensitive)
// and ^[a-z]* should match those and :Category... style links too
const omitRegex = /^[a-z]*:|^[Ss]pecial:|^[Ii]mage|^[Cc]ategory/;
const friendlyCurrentArticleName = oldTarget.toString();
const wikPos = getValueOf('popupDabWiktionary');
for (let 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) {
const wikTarget =
'wiktionary:' +
friendlyCurrentArticleName.replace(/^(.+)\s+[(][^)]+[)]\s*$/, '$1');
let meth;
if (wikPos.toLowerCase() == 'first') {
meth = 'unshift';
} else {
meth = 'push';
}
ret[meth](retargetDab(wikTarget, oldTarget, friendlyCurrentArticleName, titleToEdit));
}
ret.push(
changeLinkTargetLink({
newTarget: null,
text: popupString('remove this link').split(' ').join(' '),
hint: popupString('remove all links to this disambig page from this article'),
clickButton: getValueOf('popupDabsAutoClick'),
oldTarget: oldTarget,
summary: simplePrintf(getValueOf('popupRmDabLinkSummary'), [friendlyCurrentArticleName]),
watch: getValueOf('popupWatchDisambiggedPages'),
title: titleToEdit,
})
);
return ret;
}
function rmDupesFromSortedList(list) {
const ret = [];
for (let 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
const titleToEdit = navpop.parentPopup && navpop.parentPopup.article.toString();
const list = listLinks(data, navpop.originalArticle, titleToEdit);
if (list.length === 0) {
log('listLinks returned empty list');
return null;
}
let html = '
' + popupString('Click to disambiguate this link to:') + '
';html += list.join(', ');
return html;
}
function makeFixDabs(wikiText, navpop) {
if (
getValueOf('popupFixDabs') &&
isDisambig(wikiText, navpop.article) &&
Title.fromURL(location.href).namespaceId() != pg.nsSpecialId &&
navpop.article.talkPage()
) {
setPopupHTML(makeFixDab(wikiText, navpop), 'popupFixDab', navpop.idNumber);
}
}
function popupRedlinkHTML(article) {
return changeLinkTargetLink({
newTarget: null,
text: popupString('remove this link').split(' ').join(' '),
hint: popupString('remove all links to this page from this article'),
clickButton: getValueOf('popupRedlinkAutoClick'),
oldTarget: article.toString(),
summary: simplePrintf(getValueOf('popupRedlinkSummary'), [article.toString()]),
});
}
// 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;
}
const 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(() => {
setPopupHTML(str, elementId, popupId, onSuccess);
}, 600);
}
return null;
}
function setPopupTrailer(str, id) {
return setPopupHTML(str, 'popupData', id);
}
// 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
let redir = true;
let rcid;
if (typeof args != 'object' || typeof args.redir == 'undefined' || !args.redir) {
redir = false;
}
const a = args.navpopup.parentAnchor;
let 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;
}
const x = {
article: article,
hint: hint,
oldid: oldid,
rcid: rcid,
navpop: args.navpopup,
params: params,
};
const structure = pg.structures[getValueOf('popupStructure')];
if (typeof structure != 'object') {
setPopupHTML(
'popupError',
'Unknown structure (this should never happen): ' + pg.option.popupStructure,
args.navpopup.idNumber
);
return;
}
const spans = flatten(pg.misc.layout);
const numspans = spans.length;
const redirs = pg.misc.redirSpans;
for (let i = 0; i < numspans; ++i) {
const 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;
}
const structurefn = structure[spans[i]];
if (structurefn === undefined) {
// nothing to do for this structure part
continue;
}
let 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) {
const ret = [];
if (typeof start == 'undefined') {
start = 0;
}
for (let 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');
const 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) {
let ret = '';
for (let 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(/(<\/[^>]*?>$)/).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
// where n=idNumber
function imageHTML(article, idNumber) {
return simplePrintf(
'' +
'
' +'',
[idNumber]
);
}
function popTipsSoonFn(id, when, popData) {
if (!when) {
when = 250;
}
const 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;
}
const uls = parent.getElementsByTagName('ul');
for (let 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');
const 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;
}
const x = Navpopup.tracker.x,
y = Navpopup.tracker.y;
const 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
let 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
const d = Date.now();
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(
() => {
clearInterval(navpop.stopPopupTimer);
},
'hide',
'before'
);
}
}
// ENDFILE: mouseout.js
// STARTFILE: previewmaker.js
/**
* @file
* 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 () {
const 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(
/^\n|\n(?=\n)|/g,
''
);
};
/**
* @private
*/
Previewmaker.prototype.killDivs = function () {
// say goodbye, divs (can be nested, so use * not *?)
this.data = this.data.replace(/< *div[^>]* *>[\s\S]*?< *\/ *div *>/gi, '');
};
/**
* @private
*/
Previewmaker.prototype.killGalleries = function () {
this.data = this.data.replace(/< *gallery[^>]* *>[\s\S]*?< *\/ *gallery *>/gi, '');
};
/**
* @private
*/
Previewmaker.prototype.kill = function (opening, closing, subopening, subclosing, repl) {
let oldk = this.data;
let 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
) {
const op = this.makeRegexp(opening);
const cl = this.makeRegexp(closing, '^');
const sb = subopening ? this.makeRegexp(subopening, '^') : null;
const 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;
}
let ret = '';
const opResult = op.exec(txt);
ret = txt.substring(0, opResult.index);
txt = txt.substring(opResult.index + opResult[0].length);
let depth = 1;
while (txt.length > 0) {
let 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 || '';
let reStr = '';
let flags = '';
if (isString(x)) {
reStr = prefix + literalizeRegex(x) + suffix;
} else if (isRegExp(x)) {
let s = x.toString().substring(1);
const 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(/[{][{][^{}\s|]*?(float|box)[_ ](begin|start)/i, /[}][}]\s*/, '{{');
// infoboxes etc
// from User:Zyxw/popups.js: kill frames too
this.kill(/[{][{][^{}\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(/
/i, /<\/table.*?>/i, / /i); // remove lines starting with a pipe for the hell of it (?)
this.data = this.data.replace(/^[|].*$/mg, '');
};
/**
* @private
*/
Previewmaker.prototype.killImages = function () {
const forbiddenNamespaceAliases = [];
$.each(mw.config.get('wgNamespaceIds'), (_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 ...
this.kill(/]*?>/i, /<\/ref>/i);
// let's also delete entire lines starting with <. it's worth a try.
this.data = this.data.replace(/(^|\n) *<.*/g, '\n');
// and those pesky html tags, but not
or const splitted = this.data.parenSplit(/(<[\w\W]*?(?:>|$|(?=<)))/);
const len = splitted.length;
for (let i = 1; i < len; i = i + 2) {
switch (splitted[i]) {
case '
': case '':
case '
':
':case '
break;
default:
splitted[i] = '';
}
}
this.data = splitted.join('');
};
/**
* @private
*/
Previewmaker.prototype.killChunks = function () {
// heuristics alert
// chunks of italic text? you crazy, man?
const italicChunkRegex = /((^|\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(/^-{4,}/mg, '');
// no indented lines
this.data = this.data.replace(/(^|\n) *:[^\n]*/g, '');
// replace __TOC__, __NOTOC__ and whatever else there is
// this'll probably do
this.data = this.data.replace(/^__[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
let d = this.data;
if (getValueOf('popupPreviewCutHeadings')) {
this.data = this.data.replace(/\s*(==+[^=]*==+)\s*/g, '\n\n$1 ');
/// then we want to get rid of paragraph breaks whose text ends badly
this.data = this.data.replace(/([:;]) *\n{2,}/g, '$1\n');
this.data = this.data.replace(/^[\s\n]*/, '');
const stuff = /^([^\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(/(==+[^=]*==+)\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(/([!?.]+["']*\s)/g);
// leading space is bad, mmkay?
d[0] = d[0].replace(/^\s*/, '');
const notSentenceEnds = /([^.][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;
let n = this.maxSentences;
let 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 (let i = 0; i < strs.length - 2; ++i) {
if (reg.test(strs[i])) {
const a = [];
for (let 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) {
const 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(/^ *'+ *$/gm, '');
};
/**
* Runs the various methods to generate the preview.
* The preview is stored in the
html
- ');
} else if (li == '#') {
ps('
- ');
}
// open a new dl only if the prev item is not a dl item (:, ; or empty)
else if ($.inArray(prev.charAt(matchPos), ['', '*', '#'])) {
ps('
- ');
}
}
switch (l_match[1].charAt(l_match[1].length - 1)) {
case '*':
case '#':
ps('