CSS- und Javascript-Änderungen müssen den Wiki-Designrichtlinien entsprechen.
Hinweis: Leere nach dem Speichern den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: ⇧ Umschalt drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+ F5 oder Strg+ R (⌘ Cmd+ R auf dem Mac) drücken
- Google Chrome: ⇧ Umschalt+ Strg+ R (⌘ Cmd+ ⇧ Umschalt+ R auf dem Mac) drücken
- Opera: ⇧ Umschalt+ F5 oder Strg+ F5 drücken oder zu Einstellungen → Datenschutz und Sicherheit → Browserdaten löschen → Bilder und Dateien im Cache navigieren
- Internet Explorer: Strg+ F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/*
This is the code for the "Compage Page List" gadget (MediaWiki:Gadget-comparePageList).
The gadget compares a set of pages from this wiki ("local") to a set of pages on
another wiki ("foreign"). It displays all edits of the foreign pages that have been
made after the last edit to the respective local pages.
The set of pages can be selected via a category or a namespace, or only a single
page can be selected. The connection to the foreign wiki can be via interwiki links
or direct page title matching. All of this is entered using a form displayed by
the gadget.
The gadget expects a <div> element with an "id" attribute of "cpl-app", which it
will use to fill with its content. If not present, it appends this element itself
at the bottom of the page.
The gadget was designed for the Terraria Wiki, but it can be used on other wikis
as well. Some tweaking might be necessary; see the very bottom of the code.
*/
/*
==============================
Internationalization data
==============================
*/
const l10nTable = {
en: {
// Strings in the input area
'cpl-input-pagelist-heading': 'Pages to compare',
'cpl-input-pagelist-cateplaceholder': 'Enter the category name',
'cpl-input-pagelist-catelabel': 'Category:',
'cpl-input-pagelist-nslabel': 'Namespace:',
'cpl-input-pagelist-singleplaceholder': 'Enter the page name',
'cpl-input-pagelist-singlelabel': 'Page:',
'cpl-input-pagelist-radiocate': 'All pages in a category',
'cpl-input-pagelist-radions': 'All pages in a namespace',
'cpl-input-pagelist-radiosingle': 'Single page',
'cpl-input-foreign-heading': 'Foreign wiki to compare pages to',
'cpl-input-foreign-iwlabel': 'Interwiki prefix',
'cpl-input-foreign-radiolabel': 'Matching method:',
'cpl-input-foreign-radioiw': 'Interwiki link',
'cpl-input-foreign-radiotitle': 'Page title',
'cpl-input-foreign-helpintro': 'Select how to determine the pairs of pages to compare between this wiki and the foreign wiki.',
'cpl-input-foreign-helpiw': '<em>Interwiki link</em> means that the interwiki links from the pages on ' +
'this wiki are the connection to the pages on the foreign wiki. ' +
'For instance, if a page "Foo" on this wiki contained the interwiki link <code>en:Bar</code>, ' +
'then it would be compared to the page "Bar" on the foreign wiki. ' +
'This is mostly useful for articles in the mainspace. Note that pages without an interwiki link to ' +
'the foreign wiki are disregarded when selecting this option.',
'cpl-input-foreign-helptitle': '<em>Page title</em> means that the titles of the pages on this wiki are ' +
'compared to their identically named counterparts on the foreign wiki. ' +
'For instance, the page "Foo" on this wiki would be compared to the page "Foo" on the foreign wiki. ' +
'This is useful when the set of pages does not contain interwiki links, e.g. most templates. ' +
'Pages in namespaces that do not exist on the foreign wiki are ignored.',
'cpl-input-submit-label': 'Compare pages',
'cpl-input-submit-title': 'Compare the specified set of pages on this wiki with their equivalents on the foreign wiki',
'cpl-input-submit-debug': 'Debug mode',
'cpl-input-language': 'Language',
'cpl-input-error-radiomissing': 'Field is required! Please select an option.',
'cpl-input-error-textinputmissing': 'Field is required! Please enter a value.',
'cpl-input-error-invalidcate': 'This category does not exist! Please enter the name of an existing category.',
'cpl-input-error-invalidpage': 'This page does not exist! Please enter the title of an existing page.',
'cpl-mainspace': '(Main)',
// Strings in the logging area
'cpl-log-proc-start': 'Started the process.',
'cpl-log-proc-endsuccess': 'Terminated the process successfully.',
'cpl-log-proc-enderror': 'Terminated the process due to an error.',
'cpl-log-proc-enduser': 'Terminated the process because confirmation to continue was not given.',
'cpl-log-foreignwiki': 'Located the foreign wiki (<code>$1</code>) at $2.',
'cpl-log-fetchingpages': 'Fetching the pages... ',
'cpl-log-fetchingpages-done': 'Done. ',
'cpl-log-fetchingpage': 'Fetching information about the page... ',
'cpl-log-fetchingpage-done': 'Done. ',
'cpl-log-emptycate': 'There are no pages to compare in the category.',
'cpl-log-emptyns': 'There are no pages to compare in the namespace.',
'cpl-log-foundpagescate': 'Found $1 {{plural:$1|page|pages}} in the category.',
'cpl-log-foundpagesns': 'Found $1 {{plural:$1|page|pages}} in the namespace.',
'cpl-log-foundpage': 'Found the page.',
'cpl-log-cutoff': ' (There are more pages, but $1 is the internal limit.)',
'cpl-log-removeinvalidiw': 'Removing pages that do not have an interwiki link to the <code>$1</code> wiki... ',
'cpl-log-removeinvalidtitle': 'Removing pages that do not have a counterpart on the <code>$1</code> wiki... ',
'cpl-log-removeinvalidiw-done': 'Done. ',
'cpl-log-removeinvalidtitle-done': 'Done. ',
'cpl-log-remainingpages': 'There {{plural:$1|is $1 page|are $1 pages|0=are no pages}} remaining.',
'cpl-log-fetchingrevs': 'Fetching revisions about {{plural:$1|it|them}}... ',
'cpl-log-fetchingrevs-done': 'Done. ',
'cpl-log-outdatedpages': 'There {{plural:$1|is $1 outdated page|are $1 outdated pages|0=are no outdated pages}}.',
'cpl-log-displayingresult': 'Displaying {{plural:$1|it|them}} in a table... ',
'cpl-log-displayingresult-done': 'Done.',
// Strings in the error area
'cpl-error-details': 'Error details: ',
'cpl-error-fetchingpages': 'Error while fetching pages!',
'cpl-error-foreignwiki': 'Couldn\'t locate the foreign wiki (<code>$1</code>)! Might need to fix the interwiki link at $2.',
'cpl-error-canonicalns': 'Error while getting the canonical names of the local namespaces!',
'cpl-error-foreignpages': 'Error while checking whether the pages of this wiki have a counterpart on the <code>$1</code> wiki!',
'cpl-error-foreigndiff': 'Error while attempting to fetch the size of a diff on the <code>$1</code> wiki!',
'cpl-error-collapse': 'Couldn\'t load the library that enables table collapsing!',
'cpl-error-sorter': 'Couldn\'t load the library that enables table sorting!',
'cpl-error-messages': 'Couldn\'t load strings for the recent changes-style output!',
'cpl-error-makingoutput': 'Error while displaying the output!',
// Strings in the output area
'cpl-output-nothingtodisplay': 'All pages are up-to-date!',
'cpl-output-nothingtodisplay-single': 'The page "$1" is up-to-date!',
'cpl-output-tableheadlocal': 'Latest edit on this wiki',
'cpl-output-tableheadforeign': 'Latest edits on <code>$1</code>',
'cpl-output-timezone': 'Note: All timestamps are in UTC.',
'cpl-output-warningdialog-title': 'Warning!',
'cpl-output-warningdialog-text': 'The list of pages is very long, hence the process might take some time or fail entirely. ' +
'Moreover, it might fill up your computer\'s memory (RAM) and consequently slow it down severely. Reloading the page ' +
'afterwards should free up your RAM again. Do you wish to continue?'
},
de: {
'cpl-input-pagelist-heading': 'Zu vergleichende Seiten',
'cpl-input-pagelist-cateplaceholder': 'Kategorienamen eingeben',
'cpl-input-pagelist-catelabel': 'Kategorie:',
'cpl-input-pagelist-nslabel': 'Namensraum:',
'cpl-input-pagelist-singleplaceholder': 'Seitennamen eingeben',
'cpl-input-pagelist-singlelabel': 'Seite:',
'cpl-input-pagelist-radiocate': 'Alle Seiten in einer Kategorie',
'cpl-input-pagelist-radions': 'Alle Seiten in einem Namensraum',
'cpl-input-pagelist-radiosingle': 'Einzelne Seite',
'cpl-input-foreign-heading': 'Fremdes Wiki zum Vergleichen der Seiten',
'cpl-input-foreign-iwlabel': 'Interwiki-Präfix',
'cpl-input-foreign-radiolabel': 'Zuordnung:',
'cpl-input-foreign-radioiw': 'Interwiki-Link',
'cpl-input-foreign-radiotitle': 'Seitentitel',
'cpl-input-foreign-helpintro': 'Wähle aus, auf welche Weise im fremden Wiki die Äquivalente zu den Seiten in diesem Wiki ermittelt werden sollen.',
'cpl-input-foreign-helpiw': 'long helptext iwlink',
'cpl-input-foreign-helptitle': 'long helptext pagetitle',
'cpl-input-submit-label': 'Seiten vergleichen',
'cpl-input-submit-title': 'Vergleiche die ausgewählte Menge an Seiten dieses Wikis mit ihren Äquivalenten im fremden Wiki',
'cpl-input-submit-debug': 'Debug-Modus',
'cpl-input-language': 'Sprache',
'cpl-input-error-radiomissing': 'Erforderliches Feld! Wähle eine Option aus.',
'cpl-input-error-textinputmissing': 'Erforderliches Feld! Bitte gib einen Wert ein.',
'cpl-input-error-invalidcate': 'Diese Kategorie existiert nicht! Bitte gib den Namen einer existierenden Kategorie ein.',
'cpl-input-error-invalidpage': 'Diese Seite existiert nicht! Bitte gib den Namen einer existierenden Seite ein.',
'cpl-mainspace': '(Seiten)',
'cpl-log-proc-start': 'Prozess gestartet.',
'cpl-log-proc-endsuccess': 'Prozess erfolgreich beendet.',
'cpl-log-proc-enderror': 'Prozess aufgrund eines Fehlers beendet.',
'cpl-log-proc-enduser': 'Prozess aufgrund nicht erfolgter Bestätigung beendet.',
'cpl-log-foreignwiki': 'Fremdes Wiki (<code>$1</code>) gefunden: $2.',
'cpl-log-fetchingpages': 'Sammle Seiten... ',
'cpl-log-fetchingpages-done': 'Fertig. ',
'cpl-log-fetchingpage': 'Sammle Informationen über die Seite... ',
'cpl-log-fetchingpage-done': 'Fertig. ',
'cpl-log-emptycate': 'Es gibt keine Seiten zum Vergleichen in der Kategorie.',
'cpl-log-emptyns': 'Es gibt keine Seiten zum Vergleichen in dem Namensraum.',
'cpl-log-foundpagescate': 'Es {{plural:$1|wurde $1 Seite|wurden $1 Seiten}} in der Kategorie gefunden.',
'cpl-log-foundpagesns': 'Es {{plural:$1|wurde $1 Seite|wurden $1 Seiten}} in dem Namensraum gefunden.',
'cpl-log-foundpage': 'Die Seite wurde gefunden.',
'cpl-log-cutoff': ' (Es gibt noch mehr Seiten, aber $1 ist das interne Limit.)',
'cpl-log-removeinvalidiw': 'Entferne Seiten, die keinen Interwiki-Link zum <code>$1</code>-Wiki haben... ',
'cpl-log-removeinvalidtitle': 'Entferne Seiten, die im <code>$1</code>-Wiki kein Pendant haben... ',
'cpl-log-removeinvalidiw-done': 'Fertig. ',
'cpl-log-removeinvalidtitle-done': 'Fertig. ',
'cpl-log-remainingpages': 'Es {{plural:$1|bleibt $1 Seite|bleiben $1 Seiten|0=bleiben keine Seiten}} übrig.',
'cpl-log-fetchingrevs': 'Sammle Informationen zu {{plural:$1|ihrer Versionsgeschichte|ihren Versionsgeschichten}}... ',
'cpl-log-fetchingrevs-done': 'Fertig. ',
'cpl-log-outdatedpages': 'Es {{plural:$1|gibt $1 veraltete Seite|gibt $1 veraltete Seiten|0=gibt keine veraltete Seiten}}.',
'cpl-log-displayingresult': 'Stelle sie in einer Tabelle dar... ',
'cpl-log-displayingresult-done': 'Fertig.',
'cpl-error-details': 'Fehlerdetails: ',
'cpl-error-fetchingpages': 'Fehler beim Sammeln der Informationen!',
'cpl-error-foreignwiki': 'Konnte das fremde Wiki (<code>$1</code>) nicht finden! Womöglich muss der Interwiki-Link bei $2 behoben werden.',
'cpl-error-canonicalns': 'Fehler beim Sammeln der englischen Namen der Namensräume dieses Wikis!',
'cpl-error-foreignpages': 'Fehler beim Prüfen, ob die Seiten in diesem Wiki Pendants im <code>$1</code>-Wiki haben!',
'cpl-error-foreigndiff': 'Fehler beim Abrufen der Größe des Unterschieds zwischen zwei Versionen im <code>$1</code>-Wiki!',
'cpl-error-collapse': 'Konnte die Bibliothek nicht einbinden, die das Einklappen von Tabellen ermöglicht!',
'cpl-error-messages': 'Konnte Texte für die Ausgabe im Letzte-Änderungen-Stil nicht laden!',
'cpl-error-makingoutput': 'Fehler beim Anzeigen des Resultats!',
'cpl-output-nothingtodisplay': 'Alle Seiten sind aktuell!',
'cpl-output-nothingtodisplay-single': 'Die Seite „$1“ ist aktuell!',
'cpl-output-tableheadlocal': 'Letzte Bearbeitung in diesem Wiki',
'cpl-output-tableheadforeign': 'Letzte Bearbeitungen im <code>$1</code>-Wiki',
'cpl-output-timezone': 'Hinweis: Alle Zeitstempel sind in koordinierter Weltzeit (UTC) angegeben.',
'cpl-output-warningdialog-title': 'Warnung!',
'cpl-output-warningdialog-text': 'Die Liste der Seiten ist sehr lang, daher kann der Vorgang mehr Zeit in Anspruch nehmen ' +
'oder sogar fehlschlagen. Darüber hinaus kann er große Mengen des Arbeitsspeichers deines Computers belegen und ' +
'den Computer infolgedessen bedeutend verlangsamen. Ein Neuladen der Seite gibt den Arbeitsspeicher meist wieder frei. ' +
'Möchtest du fortfahren?'
}
}
/*
==============================
Global variables
==============================
*/
// enum
const MatchingMethod = Object.freeze({
Iwlink: 'Iwlink',
Pagetitle: 'Pagetitle'
})
// enum
const PagelistMethod = Object.freeze({
Cate: 'Cate',
Ns: 'Ns',
SinglePage: 'SinglePage'
})
const apiLimitKeyName = {
Cate: 'categorymembers',
Ns: 'allpages'
}
const articlePath = mw.config.get('wgArticlePath')
const userLanguage = mw.config.get('wgUserLanguage')
let foreignUrl
const getLocalUrl = page => articlePath.replace('$1', page)
// fallback: use the local URL for foreign links
// (this is not wanted behavior, of course, and it is overwritten if the connection
// to the foreign wiki can be established successfully)
let getForeignUrl = getLocalUrl
let formInput,
debug,
pagelist,
pagelistForOutput,
nsData,
interwikiMap,
logElement,
outputElement,
progressbar,
msgWidget
/*
==============================
Internationalization utilities
==============================
*/
// see https://gerrit.wikimedia.org/g/mediawiki/core/%2B/HEAD/resources/lib/jquery.i18n/
// for documentation on jquery.i18n
const reparseUponLocalization = {}
const timestampsToLocalize = { _count: 0 }
const attributesToPrepareForI18n = []
for (const lang in l10nTable) {
// add the ⧼ ⧽ braces around each l10n key so that unregistered keys are displayed
// in the format that is familiar from normal MediaWiki system messages
for (const key in l10nTable[lang]) {
l10nTable[lang]['⧼' + key + '⧽'] = l10nTable[lang][key]
}
// register the translated language names of all registered languages
// (e.g., if "en" and "de" are registered:
// "English", "German" for "en", and "Englisch", "Deutsch" for "de")
for (const langcode in l10nTable) {
try {
const translatedLanguage = new Intl.DisplayNames(lang, {type: 'language', fallback: 'none'}).of(langcode)
if (translatedLanguage === undefined) {
throw new Error()
}
l10nTable[lang][`⧼cpl-lang-${langcode}⧽`] = translatedLanguage
} catch (e) {
// either "RangeError: Incorrect locale information provided" from the Intl.DisplayNames constructor
// or manual throw
l10nTable[lang][`⧼cpl-lang-${langcode}⧽`] = `⧼cpl-lang-${langcode}⧽`
}
}
}
// system messages used in the output,
// will be added to the l10nTable later on
const msgs = [
'brackets',
'contribslink',
'cur',
'diff',
'enhancedrc-history',
'hist',
'last',
'minoreditletter',
'nchanges',
'newpageletter',
'ntimes',
'parentheses',
'pipe-separator',
'rc-change-size-new',
'recentchanges-label-minor',
'recentchanges-label-newpage',
'semicolon-separator',
'talkpagelinktext'
]
function getCurrentLang () {
return $.i18n().locale
}
function changeLang (newLang) {
$.i18n().locale = newLang
$('#cpl-app').attr('lang', getCurrentLang())
localizeAll()
}
function localizeAll (selector = '') {
// if selector is empty, localize every single string in the DOM
// regular localization
localizeAllSimple(selector + '[data-i18n]')
// reparse upon localization
localizeAllReparse(selector + '[data-i18n-function]')
}
function localizeAllSimple (selector) {
// localize all elements which have a "data-i18n" attribute set
$(selector).each((_, elem) => {
const l10nData = $(elem).data('i18n')
const l10nParams = $(elem).data('i18n-parameters') || []
if (l10nData.startsWith('⧼') || l10nData.startsWith('[html]⧼')) {
// this element needs localization of its HTML content
const l10nKey = l10nData
$(elem).html($.i18n(l10nKey, ...l10nParams))
} else {
// this element needs localization of one of its attributes
const attrName = l10nData.slice(1, l10nData.indexOf(']'))
const l10nKey = l10nData.slice(l10nData.indexOf('⧼'))
$(elem).attr(attrName, $.i18n(l10nKey, ...l10nParams))
}
})
}
function localizeAllReparse (selector) {
// some elements have nested localization and need to be "re-parsed" after
// having been localized, so a simple localization key is not sufficient
// for them. therefore, instead of simply setting a "data-i18n"
// attribute, they set a function that is to be executed upon
// localization, i.e. here
$(selector).each((_, elem) => {
const fullFunctionName = $(elem).data('i18n-function')
const [functionName, index] = fullFunctionName.split('/')
$(elem).html(reparseUponLocalization[functionName][index]())
localizeAll('[data-i18n-function="' + fullFunctionName + '"] ')
})
}
function localizedTextRaw (key, ...parameters) {
return $.i18n(`⧼${key}⧽`, ...parameters)
}
function localizedText (key, ...parameters) {
const spanElem = $('<span>').attr('data-i18n', `⧼${key}⧽`)
if (parameters.length > 0) {
spanElem.attr('data-i18n-parameters', JSON.stringify(parameters))
}
return spanElem
}
function localizedHtmlSnippet (key, ...parameters) {
return new OO.ui.HtmlSnippet(localizedText(key, ...parameters)[0].outerHTML)
}
function localizedAttribute (attrName, key, ...parameters) {
// $.i18n recognizes arbitrary HTML attribute names in the brackets
// (not just the string "html"; this is not mentioned in the documentation),
// see https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/HEAD/resources/lib/jquery.i18n/src/jquery.i18n.js#242
// attributes cannot be localized immediately if their elements
// don't exist in the DOM yet (which is often the case with OOUI),
// so the l10n information gets stored to a global array first, and
// the function that transfers it from that array to the element
// needs to be called when the element exists in the DOM
if (!attributesToPrepareForI18n.includes(attrName)) {
attributesToPrepareForI18n.push(attrName)
}
return `[${attrName}]⧼${key}⧽${JSON.stringify(parameters)}`
}
function prepareAttributesForI18n () {
// take the information stored in the global array and transfer
// it to each element
attributesToPrepareForI18n.forEach(attrName => {
$(`[${attrName}^="[${attrName}]⧼"]`).each((_, elem) => {
const l10nData = $(elem).attr(attrName)
// split the l10n information into key and parameters
const splitpos = l10nData.indexOf('⧽') + 1
const l10nKey = l10nData.slice(0, splitpos)
const l10nParams = l10nData.slice(splitpos)
$(elem).attr('data-i18n', l10nKey)
if (l10nParams !== '[]') {
$(elem).attr('data-i18n-parameters', l10nParams)
}
})
})
}
function localizedTextWithReparse (elemName, func) {
if (reparseUponLocalization[elemName] === undefined) {
reparseUponLocalization[elemName] = []
}
const index = reparseUponLocalization[elemName].length
reparseUponLocalization[elemName].push(func)
const funcName = elemName + '/' + index
const span = $('<span>')
.attr('data-i18n-function', funcName)
.html(func())
return span[0].outerHTML
}
function localizedTimestamp (timestamps) {
const newI18nKey = `⧼timestamp-${++timestampsToLocalize['_count']}⧽`
for (const lang of Object.keys(timestamps)) {
if (timestampsToLocalize[lang] === undefined) {
timestampsToLocalize[lang] = {}
}
timestampsToLocalize[lang][newI18nKey] = timestamps[lang]
}
return $('<span>')
.attr('data-i18n', newI18nKey)
}
function localizedNumber (number, numberformat) {
return localizedTextWithReparse('formatNumber', () => {
let numformat
switch (numberformat) {
case 'withsign':
numformat = new Intl.NumberFormat(getCurrentLang(), { signDisplay: 'exceptZero' })
break
default:
numformat = new Intl.NumberFormat(getCurrentLang())
break
}
// return number
return numformat.format(number)
})
}
/*
==============================
Parameters for API queries
==============================
*/
class ApiQueryParams {
static allCates () {
return {
action: 'query',
list: 'allcategories',
aclimit: 'max'
}
}
static localInterwikis () {
return {
action: 'query',
meta: 'siteinfo',
siprop: 'interwikimap',
sifilteriw: 'local'
}
}
static siteinfo () {
return {
action: 'query',
meta: 'siteinfo'
}
}
static localNamespaces () {
// use mw.config.get("wgFormattedNamespaces") for localized namespaces,
// this API call should be used for getting the canonical names
return {
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces'
}
}
static parseTemplate (text) {
return {
action: 'parse',
text: text,
contentmodel: 'wikitext',
disablelimitreport: true,
wrapoutputclass: '',
prop: 'text'
}
}
static pagesExistence (titles) {
return {
action: 'query',
titles: titles,
prop: 'info'
}
}
static cateMembers (lllang, gcmtitle) {
return {
action: 'query',
prop: 'langlinks|revisions',
lllang: lllang,
lllimit: 'max',
generator: 'categorymembers',
gcmtitle: gcmtitle,
gcmlimit: 'max',
gcmsort: 'timestamp',
gcmdir: 'older',
rvprop: 'ids|timestamp|flags|parsedcomment|user|size'
}
}
static allPagesInNamespace (lllang, gapnamespace) {
return {
action: 'query',
prop: 'langlinks|revisions',
lllang: lllang,
lllimit: 'max',
generator: 'allpages',
gapnamespace: gapnamespace,
gaplimit: 'max',
rvprop: 'ids|timestamp|flags|parsedcomment|user|size'
}
}
static singlePage (lllang, title) {
return {
action: 'query',
prop: 'langlinks|revisions',
lllang: lllang,
lllimit: 'max',
titles: title,
rvprop: 'ids|timestamp|flags|parsedcomment|user|size'
}
}
static diffSize (fromrev, torev) {
return {
action: 'compare',
fromrev: fromrev,
torev: torev,
prop: 'size'
}
}
static revisions (rvend, titles) {
return {
action: 'query',
prop: 'revisions',
rvprop: 'ids|timestamp|flags|parsedcomment|user|size',
rvstart: 'now',
rvend: rvend,
titles: titles,
rvlimit: 'max'
}
}
}
/*
==============================
Initializing
==============================
*/
// class that provides a function to load local categories and interwiki links
// these are required to load before making the input form, which gives cates and iwlinks as input options
class InputFormPreparer {
makeCateAndIwList () {
return new Promise((resolve, reject) => {
Promise.all([
new mw.Api().get(ApiQueryParams.allCates()),
new mw.Api().get(ApiQueryParams.localInterwikis())
]).then(apiresults => {
resolve({
cateList: InputFormPreparer._catenameArrayFromApiresult(apiresults[0]),
iwList: InputFormPreparer._iwlinkObjectFromApiresult(apiresults[1])
})
})
})
}
static _catenameArrayFromApiresult (apiResult) {
if (apiResult.query !== undefined && apiResult.query.allcategories !== undefined) {
return apiResult.query.allcategories.map(cateObj => cateObj['*'])
}
return []
}
static _iwlinkObjectFromApiresult (apiResult) {
if (apiResult.query === undefined || apiResult.query.interwikimap === undefined) {
return []
}
const localUrl = mw.config.get('wgServer') + mw.config.get('wgArticlePath')
const iwList = []
apiResult.query.interwikimap.forEach(iwObj => {
// skip interwiki links that are not interlanguage links
// and skip interwiki links that point to the local wiki
if (iwObj.language !== undefined && iwObj.url !== localUrl) {
iwList.push({ prefix: iwObj.prefix, url: iwObj.url })
}
})
return iwList
}
}
// hide the "no JS" text and prepare the DOM structure
$(document).ready(() => {
$('.cpl-nojs').hide()
if (document.getElementById('cpl-app') === null) {
$('.page #content > #mw-content-text > .mw-parser-output').append(
$('<div>').attr('id', 'cpl-app')
)
}
$('#cpl-app')
.html('') // empty the element
.append(
$('<div>').attr('id', 'comparepagelist-langbutton'),
$('<div>').attr('id', 'comparepagelist-inputarea'),
$('<div>').attr('id', 'progresslogarea-progresslogarea').append(
$('<div>').attr('id', 'comparepagelist-progressbar'),
$('<div>').attr('id', 'comparepagelist-progresslog')
),
$('<div>').attr('id', 'comparepagelist-outputarea')
)
})
// load dependencies
Promise.all([
// load OOUI
mw.loader.using('oojs-ui'),
// load jquery.i18n and initialize the l10n table
mw.loader.using('jquery.i18n').done(() => $.i18n().load(l10nTable)),
// get the local result of {{lang|}} (terraria-specific, will be skipped on other wikis)
new mw.Api().get(ApiQueryParams.parseTemplate('{{lang|}}')),
// load the category and interwiki lists
new InputFormPreparer().makeCateAndIwList().then(cateAndIwList => {
console.log(`[Compare page list v${cplVersion}] Category and interwiki list on this wiki:`, cateAndIwList)
return cateAndIwList
})
]).then(data => {
// when done, set the language and start making the input form
// (data is an array of the results of the promises above)
let initialLang
if (data[2].parse !== undefined && data[2].parse.text !== undefined && data[2].parse.text['*'] !== undefined) {
let langFromTemplate = $(data[2].parse.text['*']).html().trim()
initialLang = (langFromTemplate in l10nTable ? langFromTemplate : '')
}
$.i18n().locale = initialLang || userLanguage
makeInputsAndAddThemToPage(data[3].cateList, data[3].iwList)
}).catch(error => {
console.error(error)
})
/*
==============================
Making the input form
==============================
*/
// entry function
function makeInputsAndAddThemToPage (cateList, iwList) {
InputHelper.loadCSS()
/*
* Import source of MessageWidget and ButtonMenuSelectWidget.
* These widgets are not included in Fandom's OOUI fork, apparently
* (though the styles for the MessageWidget are, for some reason).
* Sources are minified from:
* https://doc.wikimedia.org/mediawiki-core/master/js/source/oojs-ui-core.html#OO-ui-MessageWidget
* https://doc.wikimedia.org/mediawiki-core/master/js/source/oojs-ui-widgets.html#OO-ui-ButtonMenuSelectWidget
*/
OO.ui.MessageWidget=function(e){e=e||{},OO.ui.MessageWidget.super.call(this,e),OO.ui.mixin.IconElement.call(this,e),OO.ui.mixin.LabelElement.call(this,e),OO.ui.mixin.TitledElement.call(this,e),OO.ui.mixin.FlaggedElement.call(this,e),this.setType(e.type),this.setInline(e.inline),e.icon&&this.setIcon(e.icon),this.$element.append(this.$icon,this.$label).addClass("oo-ui-messageWidget")},OO.inheritClass(OO.ui.MessageWidget,OO.ui.Widget),OO.mixinClass(OO.ui.MessageWidget,OO.ui.mixin.IconElement),OO.mixinClass(OO.ui.MessageWidget,OO.ui.mixin.LabelElement),OO.mixinClass(OO.ui.MessageWidget,OO.ui.mixin.TitledElement),OO.mixinClass(OO.ui.MessageWidget,OO.ui.mixin.FlaggedElement),OO.ui.MessageWidget.static.iconMap={notice:"infoFilled",error:"error",warning:"alert",success:"check"},OO.ui.MessageWidget.prototype.setInline=function(e){e=!!e,this.inline!==e&&(this.inline=e,this.$element.toggleClass("oo-ui-messageWidget-block",!this.inline))},OO.ui.MessageWidget.prototype.setType=function(e){-1===Object.keys(this.constructor.static.iconMap).indexOf(e)&&(e="notice"),this.type!==e&&(this.clearFlags(),this.setFlags(e),this.setIcon(this.constructor.static.iconMap[e]),this.$icon.removeClass("oo-ui-image-"+this.type),this.$icon.addClass("oo-ui-image-"+e),"error"===e?(this.$element.attr("role","alert"),this.$element.removeAttr("aria-live")):(this.$element.removeAttr("role"),this.$element.attr("aria-live","polite")),this.type=e)}
OO.ui.ButtonMenuSelectWidget=function(e){e=e||{},OO.ui.ButtonMenuSelectWidget.super.call(this,e),this.$overlay=(!0===e.$overlay?OO.ui.getDefaultOverlay():e.$overlay)||this.$element,this.clearOnSelect=!1!==e.clearOnSelect,this.menu=new OO.ui.MenuSelectWidget($.extend({widget:this,$floatableContainer:this.$element},e.menu)),this.connect(this,{click:"onButtonMenuClick"}),this.getMenu().connect(this,{select:"onMenuSelect",toggle:"onMenuToggle"}),this.$button.attr({"aria-expanded":"false","aria-haspopup":"true","aria-owns":this.menu.getElementId()}),this.$element.addClass("oo-ui-buttonMenuSelectWidget"),this.$overlay.append(this.menu.$element)},OO.inheritClass(OO.ui.ButtonMenuSelectWidget,OO.ui.ButtonWidget),OO.ui.ButtonMenuSelectWidget.prototype.getMenu=function(){return this.menu},OO.ui.ButtonMenuSelectWidget.prototype.onMenuSelect=function(e){this.clearOnSelect&&e&&this.getMenu().selectItem()},OO.ui.ButtonMenuSelectWidget.prototype.onMenuToggle=function(e){this.$element.toggleClass("oo-ui-buttonElement-pressed",e)},OO.ui.ButtonMenuSelectWidget.prototype.onButtonMenuClick=function(){this.menu.toggle()};
// make fieldsets and buttons
const langButton = InputHelper.makeLangButton()
const pagelistFieldset = new PagelistFieldset(cateList)
const foreignwikiFieldset = new ForeignwikiFieldset(iwList)
const debugButtonFieldset = InputHelper.makeDebugButtonFieldset()
const submitButton = InputHelper.makeSubmitButton()
debugButtonFieldset.toggle(false) // hide initially
// display the input form on the page
$('#comparepagelist-langbutton').append(langButton.$element)
$('#comparepagelist-inputarea').append(
pagelistFieldset.fieldset.$element,
foreignwikiFieldset.fieldset.$element,
debugButtonFieldset.$element,
new OO.ui.FieldsetLayout({ items: [submitButton] }).$element
)
prepareAttributesForI18n()
localizeAll()
makeInterwikiMap(iwList)
// define logic for the submit button
submitButton.on('click', () => {
onSubmit(pagelistFieldset, foreignwikiFieldset, debugButtonFieldset)
})
}
class InputHelper {
static loadCSS () {
// load CSS used on Special:RecentChanges
// TODO: Move to gadget definition
const modules = [
'jquery.tablesorter.styles',
'mediawiki.feedlink', // the short form "mediawiki.feedlink,helplink,icon" doesn't seem to work ("Unknown module")
'mediawiki.helplink',
'mediawiki.icon',
'mediawiki.interface.helpers.styles',
'mediawiki.rcfilters.filters.base.styles',
'mediawiki.special.changeslist',
'mediawiki.special.changeslist.enhanced',
'oojs-ui.styles.icons-location'
]
mw.loader.load(modules, 'text/css')
const skin = mw.config.get('skin')
// construct URL for logging the load() action above
const urlParams = {
only: 'styles',
modules: modules.join('|'),
lang: userLanguage,
skin: skin
}
const url = mw.config.get('wgServer') + mw.config.get('wgLoadScript') + '?' + $.param(urlParams)
console.log(`[Compare page list v${cplVersion}] Loaded modules:`, url)
}
// get the value of a radiobutton group
static getSelectedInfoOfRadiobuttons (radiobuttons) {
let data
radiobuttons.items.forEach(item => {
if (item.isSelected()) {
data = item.data
}
})
return data
}
static makeLangButton () {
let currentLangOption
const langOptions = []
for (const lang in l10nTable) {
const langOptionLabel = localizedTextWithReparse('langButton', () => {
const labelHtml = $('<span>')
.append($('<span>')
.attr('lang', lang)
.text(l10nTable[lang][`⧼cpl-lang-${lang}⧽`])
)
if (getCurrentLang() !== lang) {
labelHtml.append(
' ',
$('<small>')
.addClass(noteClasses)
.html(localizedText(`cpl-lang-${lang}`))
)
}
return labelHtml[0].outerHTML
})
const newLangOption = new OO.ui.MenuOptionWidget({
data: lang,
label: new OO.ui.HtmlSnippet(langOptionLabel)
})
if (lang == getCurrentLang()) {
currentLangOption = newLangOption
}
langOptions.push(newLangOption)
}
const langButtonWidget = new OO.ui.ButtonMenuSelectWidget({
icon: 'language',
label: localizedText('cpl-input-language'),
invisibleLabel: true,
clearOnSelect: false,
menu: {
horizontalPosition: 'end',
items: langOptions.sort((a, b) => (a.getData() < b.getData() ? -1 : 1))
}
})
langButtonWidget.getMenu().on('select', (selectedItem) => {
const newLang = selectedItem.getData()
if (debug) { console.log('Changed language to ' + newLang) }
changeLang(newLang)
})
langButtonWidget.getMenu().selectItem(currentLangOption)
return langButtonWidget
}
static makeDebugButtonFieldset () {
return new OO.ui.FieldsetLayout({
items: [
new OO.ui.ToggleButtonWidget({
label: localizedHtmlSnippet('cpl-input-submit-debug'),
invisibleLabel: true,
title: localizedHtmlSnippet('cpl-input-submit-debug'),
icon: 'robot'
})
]
})
}
static makeSubmitButton () {
return new OO.ui.ButtonWidget({
label: localizedHtmlSnippet('cpl-input-submit-label'),
title: localizedHtmlSnippet('cpl-input-submit-title'),
flags: ['primary', 'progressive']
})
}
}
// class that makes the "Pages to compare" section
class PagelistFieldset {
constructor (cateList) {
this.makeCateInput(cateList) // the category selection text input field
this.makeNsInput() // the namespace selection dropdown menu
this.makeSinglePageInput() // the page selection text input field
this.makeRadioButtons(cateList) // the radio buttons to select "category", "namespace", or "single page"
this.combineAll()
}
makeCateInput (cateList) {
// see https://www.mediawiki.org/wiki/OOUI/Elements/Lookup
function LookupTextInputWidget (config) {
OO.ui.TextInputWidget.call(this,
$.extend({
placeholder: localizedAttribute('placeholder', 'cpl-input-pagelist-cateplaceholder'),
validate: catename => cateList.indexOf(catename) != -1
}, config)
)
OO.ui.mixin.LookupElement.call(this, config)
}
OO.inheritClass(LookupTextInputWidget, OO.ui.TextInputWidget)
OO.mixinClass(LookupTextInputWidget, OO.ui.mixin.LookupElement)
LookupTextInputWidget.prototype.getLookupRequest = function () {
const value = this.getValue()
const deferred = $.Deferred()
const response = []
cateList.forEach(cateName => {
if (cateName.toLowerCase().startsWith(value.toLowerCase())) {
response.push(cateName)
}
})
deferred.resolve(response)
return deferred.promise({ abort: function () {} })
}
LookupTextInputWidget.prototype.getLookupCacheDataFromResponse = function (response) {
return response || []
}
LookupTextInputWidget.prototype.getLookupMenuOptionsFromData = function (data) {
const items = []
data.forEach(cateName => {
const newItem = new OO.ui.MenuOptionWidget({
data: cateName,
label: cateName
})
items.push(newItem)
})
return items
}
this.cateInput = new LookupTextInputWidget()
this.cateInputWrapper = new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({ label: localizedHtmlSnippet('cpl-input-pagelist-catelabel') }),
this.cateInput
]
})
this.cateInputWrapper.toggle(false) // hide initially
}
makeNsInput () {
const nsOptions = []
const data = mw.config.get('wgFormattedNamespaces')
for (const nsIndex in data) {
if (nsIndex >= 0) {
nsOptions.push({
data: nsIndex,
label: (nsIndex === 0 || nsIndex === '0' ? localizedHtmlSnippet('cpl-mainspace') : data[nsIndex])
})
}
}
this.nsInput = new OO.ui.DropdownInputWidget({ options: nsOptions })
this.nsInputWrapper = new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({ label: localizedHtmlSnippet('cpl-input-pagelist-nslabel') }),
this.nsInput
]
})
this.nsInputWrapper.toggle(false) // hide initially
}
static checkPageExistence (pagename) {
if (debug) { console.log(`Checking existence of the page ${pagename}...`) }
const deferred = $.Deferred()
// check if the page exists on the wiki
new mw.Api().get(ApiQueryParams.pagesExistence(pagename))
.done(result => {
deferred.resolve(result.query && result.query.pages && Object.keys(result.query.pages)[0] > -1)
})
.fail(() => {
deferred.resolve(false)
})
return deferred
}
makeSinglePageInput () {
this.singlePageInput = new OO.ui.TextInputWidget({
icon: 'articleNotFound',
required: true,
placeholder: localizedAttribute('placeholder', 'cpl-input-pagelist-singleplaceholder'),
validate: pagename => PagelistFieldset.checkPageExistence(pagename)
})
this.singlePageInputWrapper = new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({ label: localizedHtmlSnippet('cpl-input-pagelist-singlelabel') }),
this.singlePageInput
]
})
this.singlePageInputWrapper.toggle(false) // hide initially
// change the icon that is displayed at the beginning of the input
this.singlePageInput.on('change', () => {
this.singlePageInput.getValidity()
.then(() => {
this.singlePageInput.setIcon('articleCheck') // input is a valid page name
})
.catch(() => {
this.singlePageInput.setIcon('articleNotFound')// input is not a valid page name
})
})
}
makeRadioButtons (cateList) {
const radioButtonCate = new OO.ui.RadioOptionWidget({
data: PagelistMethod.Cate,
label: localizedHtmlSnippet('cpl-input-pagelist-radiocate')
})
const radioButtonNs = new OO.ui.RadioOptionWidget({
data: PagelistMethod.Ns,
label: localizedHtmlSnippet('cpl-input-pagelist-radions')
})
const radioButtonSinglePage = new OO.ui.RadioOptionWidget({
data: PagelistMethod.SinglePage,
label: localizedHtmlSnippet('cpl-input-pagelist-radiosingle')
})
this.radiobuttons = new OO.ui.RadioSelectWidget({ items: [radioButtonCate, radioButtonNs, radioButtonSinglePage] })
this.radiobuttons.on('select', () => {
// when the radio button selection changes:
this.nsInputWrapper.toggle(radioButtonNs.isSelected()) // show/hide the namespace selection dropdown menu
this.cateInputWrapper.toggle(radioButtonCate.isSelected()) // show/hide the category selection text input field
this.cateInput.setRequired(radioButtonCate.isSelected()) // set the category selection text input field to required/optional
this.cateInput.setValidation((radioButtonCate.isSelected() ? catename => cateList.indexOf(catename) != -1 : null))
this.singlePageInputWrapper.toggle(radioButtonSinglePage.isSelected()) // show/hide the single page selection text input field
this.singlePageInput.setRequired(radioButtonSinglePage.isSelected()) // set the single page selection text input field to required/optional
this.singlePageInput.setValidation((radioButtonSinglePage.isSelected() ? pagename => PagelistFieldset.checkPageExistence(pagename) : null))
})
}
combineAll () {
const combineRadioLookupAndDropdown = new OO.ui.Widget({
content: [this.radiobuttons, this.cateInputWrapper, this.nsInputWrapper, this.singlePageInputWrapper]
})
this.fieldlayout = new OO.ui.FieldLayout(combineRadioLookupAndDropdown, {
align: 'inline' // to make potential errors appear properly at the bottom
})
this.radiobuttons.on('select', () => {
// when the radio button selection changes, remove potentially set error message about missing radio buttion selection
// because now a radio button is selected for sure
this.fieldlayout.setErrors((this.pagelistInput.method === undefined ? [localizedText('cpl-input-error-radiomissing')] : []))
localizeAll(this.fieldlayout.id)
})
this.fieldset = new OO.ui.FieldsetLayout({
label: localizedHtmlSnippet('cpl-input-pagelist-heading'),
items: [this.fieldlayout]
})
}
get pagelistInput () {
const method = InputHelper.getSelectedInfoOfRadiobuttons(this.radiobuttons)
let details
if (method !== undefined) {
const inputElemBasedOnPagelistMethod = {}
inputElemBasedOnPagelistMethod[PagelistMethod.Cate] = this.cateInput
inputElemBasedOnPagelistMethod[PagelistMethod.Ns] = this.nsInput
inputElemBasedOnPagelistMethod[PagelistMethod.SinglePage] = this.singlePageInput
details = inputElemBasedOnPagelistMethod[method].value
}
return {
method: method,
details: details
}
}
}
// class that makes the "Foreign wiki to compare pages to" section
class ForeignwikiFieldset {
constructor (iwList) {
this.makeIwInput(iwList)
this.makeRadioButtons()
this.combineAll()
}
makeIwInput (iwList) {
const iwOptions = []
iwList.forEach(iwObj => {
iwOptions.push({
data: iwObj.prefix,
label: iwObj.prefix
})
})
this.iwInput = new OO.ui.DropdownInputWidget({
options: iwOptions,
value: 'en' // select "en" by default, will fallback to the first option if "en" is not available
})
this.iwInputWrapper = new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({
label: $('<a>')
.attr('href', getLocalUrl('Special:Interwiki'))
.append(localizedText('cpl-input-foreign-iwlabel'))
}),
this.iwInput
]
})
}
makeRadioButtons () {
const radioButtonIwlink = new OO.ui.RadioOptionWidget({
data: MatchingMethod.Iwlink,
label: localizedHtmlSnippet('cpl-input-foreign-radioiw')
})
const radioButtonPagetitle = new OO.ui.RadioOptionWidget({
data: MatchingMethod.Pagetitle,
label: localizedHtmlSnippet('cpl-input-foreign-radiotitle')
})
this.radiobuttons = new OO.ui.RadioSelectWidget({
items: [radioButtonIwlink, radioButtonPagetitle]
})
}
combineAll () {
const helptext = $('<span>')
const helptextkeys = [
'cpl-input-foreign-helpintro',
'cpl-input-foreign-helpiw',
'cpl-input-foreign-helptitle'
]
for (const key of helptextkeys) {
helptext.append($('<p>').html(localizedText(key)))
}
this.fieldlayout = new OO.ui.FieldLayout(this.radiobuttons, {
label: localizedHtmlSnippet('cpl-input-foreign-radiolabel'),
help: new OO.ui.HtmlSnippet(helptext[0].outerHTML),
align: 'inline'
})
this.radiobuttons.on('select', () => {
// when the radio button selection changes, remove potentially set error message about missing radio buttion selection
// because now a radio button is selected for sure
this.fieldlayout.setErrors((this.foreignwikiInput.matchmethod === undefined ? [localizedText('cpl-input-error-radiomissing')] : []))
localizeAll(this.fieldlayout.id)
})
this.fieldset = new OO.ui.FieldsetLayout({
label: localizedHtmlSnippet('cpl-input-foreign-heading'),
items: [this.iwInputWrapper, this.fieldlayout]
})
}
get foreignwikiInput () {
return {
iwprefix: this.iwInput.value,
matchmethod: InputHelper.getSelectedInfoOfRadiobuttons(this.radiobuttons)
}
}
}
function makeInterwikiMap (iwList) {
interwikiMap = {}
iwList.forEach(iwObj => {
interwikiMap[iwObj.prefix] = iwObj.url
})
}
function updateErrors (pagelistFieldset, foreignwikiFieldset) {
// display error messages if the radio buttons are not selected
pagelistFieldset.fieldlayout.setErrors((pagelistFieldset.pagelistInput.method === undefined ? [localizedText('cpl-input-error-radiomissing')] : []))
foreignwikiFieldset.fieldlayout.setErrors((foreignwikiFieldset.foreignwikiInput.matchmethod === undefined ? [localizedText('cpl-input-error-radiomissing')] : []))
localizeAll(pagelistFieldset.fieldlayout.id)
localizeAll(foreignwikiFieldset.fieldlayout.id)
// make an additional check if pagelistMethod is "Cate" or "SinglePage"
// to see if the input is valid
return new Promise((resolve, reject) => {
switch (pagelistFieldset.pagelistInput.method) {
case PagelistMethod.Cate:
pagelistFieldset.cateInput.getValidity()
.fail(() => {
pagelistFieldset.cateInput.setValidityFlag(false)
let errortext = 'cpl-input-error-textinputmissing'
if (pagelistFieldset.cateInput.getValue() !== '') {
errortext = 'cpl-input-error-invalidcate'
}
pagelistFieldset.fieldlayout.setErrors([localizedText(errortext)])
localizeAll(pagelistFieldset.fieldlayout.id)
pagelistFieldset.cateInput.scrollElementIntoView()
reject()
})
.done(() => { resolve() })
break
case PagelistMethod.SinglePage:
pagelistFieldset.singlePageInput.getValidity()
.fail(() => {
pagelistFieldset.singlePageInput.setValidityFlag(false)
let errortext = 'cpl-input-error-textinputmissing'
if (pagelistFieldset.singlePageInput.getValue() !== '') {
errortext = 'cpl-input-error-invalidpage'
}
pagelistFieldset.fieldlayout.setErrors([localizedHtmlSnippet(errortext)])
localizeAll(pagelistFieldset.fieldlayout.id)
pagelistFieldset.singlePageInput.scrollElementIntoView()
reject()
})
.done(() => { resolve() })
break
default:
// the pagelistMethod that was selected does not require additional validation
resolve()
break
}
})
}
function onSubmit (pagelistFieldset, foreignwikiFieldset, debugButtonFieldset) {
updateErrors(pagelistFieldset, foreignwikiFieldset)
.then(() => {
// input is valid
if (pagelistFieldset.pagelistInput.method !== undefined && foreignwikiFieldset.foreignwikiInput.matchmethod !== undefined) {
const dataForCompare = {
foreignWiki: foreignwikiFieldset.foreignwikiInput.iwprefix,
matchingMethod: foreignwikiFieldset.foreignwikiInput.matchmethod,
pagelistMethod: pagelistFieldset.pagelistInput.method,
debug: debugButtonFieldset.items[0].getValue()
}
dataForCompare[dataForCompare.pagelistMethod] = pagelistFieldset.pagelistInput.details
if (dataForCompare.debug) { console.log('Starting to compare. Data:', dataForCompare) }
startCompare(dataForCompare) // start actions
}
})
.catch(() => {}) // input is not valid, do not start actions
}
/*
==============================
Doing the comparison
==============================
*/
function startCompare (data) {
// initialize the global variables
logElement = document.getElementById('comparepagelist-progresslog')
outputElement = document.getElementById('comparepagelist-outputarea')
// reset output
logElement.innerHTML = ''
outputElement.innerHTML = ''
// reset progressbar
document.getElementById('comparepagelist-progressbar').innerHTML = '' // clear the wrapper of the actual progressbar
progressbar = new OO.ui.ProgressBarWidget({ progress: 0 }) // make new progressbar
$('#comparepagelist-progressbar').append(progressbar.$element) // display new progressbar
// progressbar.pushPending(); // doesn't seem to have any effect
// display new log box
msgWidget = new OO.ui.MessageWidget({
type: 'notice',
label: localizedHtmlSnippet('cpl-log-proc-start')
})
$(logElement).append(msgWidget.$element)
// initialize the global variables
formInput = data
debug = data.debug
pagelist = new Pagelist()
pagelistForOutput = []
nsData = {}
// start actions
doCompare()
.then(() => outputLog(localizedText('cpl-log-proc-endsuccess')))
.catch(errReason => outputLog(localizedText(errReason)))
.finally(() => {
if (debug) {
console.log($.i18n().messageStore.messages)
console.log(timestampsToLocalize)
console.log(reparseUponLocalization)
}
})
}
// function with the full flow of actions, as a chain of promises
function doCompare () {
return new Promise((resolve, reject) => {
pingForeignWiki()
// (1) when successfully connected to foreign wiki:
.then(foreignMainpage => {
try {
outputLog(localizedText('cpl-log-foreignwiki', formInput.foreignWiki, `<a href="${foreignMainpage}">${foreignMainpage}</a>`))
outputLog(localizedText((formInput.pagelistMethod === PagelistMethod.SinglePage ? 'cpl-log-fetchingpage' : 'cpl-log-fetchingpages')))
progressbar.setProgress(10)
return pagelist.getLocalPages()
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (2) when successfully fetched list of local pages:
.then(() => {
try {
progressbar.setProgress(20)
if (debug) { console.log(`Got list of local pages (${pagelist.localPagesCount}):`, pagelist) }
outputLog(localizedText((formInput.pagelistMethod === PagelistMethod.SinglePage ? 'cpl-log-fetchingpage-done' : 'cpl-log-fetchingpages-done')))
if (pagelist.localPagesCount <= 0) {
switch (formInput.pagelistMethod) {
case PagelistMethod.Cate:
addToLastLog(localizedText('cpl-log-emptycate'))
break
case PagelistMethod.Ns:
addToLastLog(localizedText('cpl-log-emptyns'))
break
default:
break
}
return breakOutOfPromiseChain()
}
switch (formInput.pagelistMethod) {
case PagelistMethod.Cate:
addToLastLog(localizedText('cpl-log-foundpagescate', pagelist.localPagesCount))
break
case PagelistMethod.Ns:
addToLastLog(localizedText('cpl-log-foundpagesns', pagelist.localPagesCount))
break
case PagelistMethod.SinglePage:
addToLastLog(localizedText('cpl-log-foundpage'))
break
default:
break
}
addToLastLog(stringAboutLimitIfNecessary())
progressbar.setProgress(25)
switch (formInput.matchingMethod) {
case MatchingMethod.Iwlink:
outputLog(localizedText('cpl-log-removeinvalidiw', formInput.foreignWiki))
break
case MatchingMethod.Pagetitle:
outputLog(localizedText('cpl-log-removeinvalidtitle', formInput.foreignWiki))
break
default:
break
}
return loadNamespaces()
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (3) when successfully fetched list of local canonical namespaces:
.then(() => {
try {
progressbar.setProgress(30)
if (debug) { console.log('Namespace list on this wiki:', nsData) }
return pagelist.addForeignTitles() // this is a pretty brief operation, no API requests
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (4) when successfully added the foreign titles to the pagelist:
.then(() => {
try {
if (debug) { console.log(`Added foreign titles. pagelist (${pagelist.localPagesCount}):`, pagelist) }
let canContinue = Promise.resolve()
if (pagelist.localPagesCount > 200) {
// if there are many pages, then only continue after user gives confirmation
canContinue = confirmationDialog(localizedTextRaw('cpl-output-warningdialog-text'), localizedTextRaw('cpl-output-warningdialog-title'))
}
return canContinue.then(() => pagelist.filterOutPagesWithInvalidForeignTitles())
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (5) when successfully removed pages without interwiki links/foreign counterparts:
.then(() => {
try {
progressbar.setProgress(40)
if (debug) { console.log(`Filtered out PagesWithInvalidForeignTitles. pagelist (${pagelist.localPagesCount}):`, pagelist) }
switch (formInput.pagelistMethod) {
case PagelistMethod.Cate:
addToLastLog(localizedText('cpl-log-removeinvalidiw-done'))
break
case PagelistMethod.Ns:
addToLastLog(localizedText('cpl-log-removeinvalidtitle-done'))
break
default:
break
}
if (pagelist.localPagesCount <= 0) {
addToLastLog(localizedText('cpl-log-remainingpages', 0))
return breakOutOfPromiseChain()
}
addToLastLog(localizedText('cpl-log-remainingpages', pagelist.localPagesCount))
outputLog(localizedText('cpl-log-fetchingrevs', pagelist.localPagesCount))
return pagelist.addForeignRevsAndLocalDiffsizes(new PerPageProgress(40, 75))
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (6) when successfully fetched diffsizes of the local pages and revisions for the foreign pages:
.then(() => {
try {
if (debug) { console.log(`Added foreign revisions and local diff sizes. pagelist (${pagelist.localPagesCount}):`, pagelist) }
return pagelist.dropNonOutdatedPages() // this is a pretty brief operation, no API requests
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (7) when successfully removed pages whose foreign pages have no newer revisions:
.then(() => {
try {
progressbar.setProgress(80)
if (debug) { console.log(`Dropped non-outdated pages. pagelist (${pagelist.localPagesCount}):`, pagelist) }
addToLastLog(localizedText('cpl-log-fetchingrevs-done'))
if (pagelist.localPagesCount <= 0) {
addToLastLog(localizedText('cpl-log-outdatedpages', 0))
if (formInput.pagelistMethod !== PagelistMethod.SinglePage) {
outputSuccess(localizedText('cpl-output-nothingtodisplay'))
} else {
outputSuccess(localizedText('cpl-output-nothingtodisplay-single', formInput.SinglePage))
}
return breakOutOfPromiseChain()
}
addToLastLog(localizedText('cpl-log-outdatedpages', pagelist.localPagesCount))
outputLog(localizedText('cpl-log-displayingresult', pagelist.localPagesCount))
return pagelist.prepareForOutput(new PerPageProgress(80, 90))
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (8) when successfully formatted the information that is specifically needed for output:
.then(() => {
try {
if (debug) { console.log('Prepared pagelist for output. pagelistForOutput:', pagelistForOutput) }
return displayOutput()
} catch (e) { return breakOutOfPromiseChain(e) }
})
// (9) when successfully created the HTML for the table:
.then(() => {
try {
if (debug) { console.log('Made output HTML.') }
addToLastLog(localizedText('cpl-log-displayingresult-done'))
return breakOutOfPromiseChain() // successfully finished all operations
} catch (e) { return breakOutOfPromiseChain(e) }
})
.catch(error => {
// progressbar.popPending(); // doesn't seem to have any effect
if (error !== undefined) {
if (debug) { console.error(error) }
progressbar.setDisabled(true)
reject(error instanceof FailedConfirmationError ? 'cpl-log-proc-enduser' : 'cpl-log-proc-enderror')
} else {
progressbar.setProgress(100)
resolve()
}
})
})
}
// class that holds the list of pages, which is altered as part of the comparison process
class Pagelist {
constructor () {
this.rawApiResult = {}
this.pages = {} // a copy of rawApiResult that will be modified and extended (e.g. with foreign revisions etc.)
}
// for comparison part 1
getLocalPages () {
return new Promise((resolve, reject) => {
const apiParams = {
Cate: ApiQueryParams.cateMembers(formInput.foreignWiki, 'Category:' + formInput.Cate),
Ns: ApiQueryParams.allPagesInNamespace(formInput.foreignWiki, formInput.Ns),
SinglePage: ApiQueryParams.singlePage(formInput.foreignWiki, formInput.SinglePage)
}
if (formInput.pagelistMethod in apiParams) {
if (debug) {
console.log(
'API query for getting list of local pages:',
mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?' + $.param(apiParams[formInput.pagelistMethod])
)
}
new mw.Api().get(apiParams[formInput.pagelistMethod]).then(pagelistGetterResult => {
this.rawApiResult = pagelistGetterResult
if (pagelistGetterResult.query !== undefined && pagelistGetterResult.query.pages !== undefined) {
this.pages = pagelistGetterResult.query.pages
}
resolve()
})
} else {
errorDuringProcess(`The pagelistMethod "${formInput.pagelistMethod}" is not a valid pagelistMethod.`, localizedText('cpl-error-fetchingpages'))
reject(Error(`pagelistMethod "${formInput.pagelistMethod}" is not in ${Object.keys(apiParams)}!`))
}
})
}
// for comparison part 2
isCutOffByLimit () {
if (this.rawApiResult.continue === undefined || this.rawApiResult.limits === undefined) {
return false
}
return this.rawApiResult.limits[apiLimitKeyName[formInput.pagelistMethod]] !== undefined
}
// for comparison part 3
addForeignTitles () {
return new Promise((resolve, reject) => {
for (const pageid in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, pageid)) {
const localPageData = pagelist.pages[pageid]
switch (formInput.matchingMethod) {
case MatchingMethod.Iwlink:
// we defined lllang in the API query that returned the initial pagelist,
// so the langlinks section will either contain only our foreignWiki langlink, or no langlinks at all.
// all other langlinks are disregarded
pagelist.pages[pageid].foreignTitle = (localPageData.langlinks === undefined ? undefined : localPageData.langlinks[0]['*'])
break
case MatchingMethod.Pagetitle:
// the namespace part of localPageData.title is localized, so remove it
const titleWithoutNs = localPageData.title.slice(nsData.localNamespacesLocalized[localPageData.ns].length)
// and then prepend the canonical namespace
const normalizedTitle = nsData.localNamespacesCanonical[localPageData.ns] + titleWithoutNs
pagelist.pages[pageid].foreignTitle = normalizedTitle
break
default:
// don't add the foreign title, because it is unknown
break
}
}
}
resolve()
})
}
// for comparison part 4
filterOutPagesWithInvalidForeignTitles () {
return ValidLocalPageFilterer.filter()
}
// for comparison part 5
addForeignRevsAndLocalDiffsizes (perPageProgress) {
const promisesForAllPages = []
perPageProgress.pagecount = pagelist.localPagesCount
// iterate over all pages
for (const localPageId in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, localPageId)) {
const localPageData = pagelist.pages[localPageId]
// make a promise for this page that will be added to the Promise.all array of all pages
const addDataToThisPage = new Promise((resolve, reject) => {
// for this page, get the local diffsize and the foreign revisions
const promisesForThisPage = [
getLocalDiffsizeForOnePage(localPageData.revisions[0].parentid, localPageData.revisions[0].revid),
getForeignRevsSinceCutoff(localPageData.foreignTitle, localPageData.revisions[0].timestamp)
]
Promise.all(promisesForThisPage)
// when successfully gotten the local diffsize and the foreign revisions for this page:
.then(promisesResult => {
// if diffsize is undefined, then it means the page is a new page, so we just take its size as the diffsize
const localDiffsize = (promisesResult[0] !== undefined ? promisesResult[0] : localPageData.revisions[0].size)
const foreignrevs = promisesResult[1]
let apiRequestDelay = 0
if (true) {
// activating the delay helps spreading out the requests,
// which improves performance and sometimes prevents errors caused by rate limits.
apiRequestDelay = 1000 + Math.random() * 1500 // between 1 and 2.5 seconds
}
setTimeout(() => {
getDiffSizesForAllForeignRevsOfOnePage(foreignrevs)
.then(foreignrevsWithDiffsizes => {
pagelist.pages[localPageId].revisions[0].diffsize = localDiffsize
pagelist.pages[localPageId].foreignRevisions = foreignrevsWithDiffsizes
perPageProgress.increment()
resolve()
})
.catch(error => { reject(error) })
}, apiRequestDelay)
})
.catch(error => { reject(error) })
})
promisesForAllPages.push(addDataToThisPage)
}
}
return new Promise((resolve, reject) => {
Promise.all(promisesForAllPages)
.then(() => { resolve() })
.catch(error => { reject(error) })
})
}
// for comparison part 6
dropNonOutdatedPages () {
return new Promise((resolve, reject) => {
for (const pageid in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, pageid)) {
if (pagelist.pages[pageid].foreignRevisions === undefined) {
// there are no foreign revisions, which means the latest local revision is after the latest foreign revision.
// hence, we can remove this local page
delete pagelist.pages[pageid]
}
}
}
resolve()
})
}
// for comparison part 7
prepareForOutput (perPageProgress) {
// fill the pagelistForOutput array with objects that contain the information
// needed for the output, e.g.:
/*
{
local: {
revid: x,
timestamp: x,
isnew: x,
username: x
},
foreign: [
{
revid: x,
timestamp: x,
isnew: x,
username: x
},
{
revid: x,
timestamp: x,
isnew: x,
username: x
}
]
*/
// this info also includes timestamps, which require localization like everything else.
// we're localizing them differently, though: we're registering the localized format for each
// language here, then later (in the output maker functions) store each timestamp instance
// (i.e. a specific date or time) as a separate l10n key to the l10nTable
const dateformats = {}
const dateformatsDate = {}
const dateformatsTime = {}
for (const lang of Object.keys(l10nTable)) {
dateformats[lang] = new Intl.DateTimeFormat(lang, {
timeZone: 'UTC',
hourCycle: 'h23',
dateStyle: 'short',
timeStyle: 'short'
})
dateformatsDate[lang] = new Intl.DateTimeFormat(lang, {
timeZone: 'UTC',
dateStyle: 'short'
})
dateformatsTime[lang] = new Intl.DateTimeFormat(lang, {
timeZone: 'UTC',
hourCycle: 'h23',
hour: '2-digit',
minute: '2-digit'
})
}
perPageProgress.pagecount = pagelist.localPagesCount
for (const pageid in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, pageid)) {
const pagedata = pagelist.pages[pageid]
const pagedataForOutput = {
local: preparePagedataLocalForOutput(pagedata, dateformatsDate, dateformatsTime),
foreign: preparePagedataForeignForOutput(pagedata, dateformats, dateformatsDate, dateformatsTime)
}
pagelistForOutput.push(pagedataForOutput)
perPageProgress.increment()
}
}
pagelistForOutput.sort((a, b) => a.local.timestampMillis - b.local.timestampMillis) // sort by last edit on local wiki
return Promise.resolve()
}
get localPagesCount () {
if (this.pages !== undefined && Object.keys(this.pages).length !== undefined) {
return Object.keys(this.pages).length
}
return -1
}
}
/*
==============================
General comparison helper functions
==============================
*/
function errorDuringProcess (errordata, errortext) {
let details
if (errordata !== undefined) {
// the errordata parameter can be anything,
// try to get useful information from it
if (errordata.promise !== undefined && (errordata.responseText !== undefined || errordata.status !== undefined)) {
details = (errordata.status || '') + ' ' + (errordata.responseText || '')
} else {
if (errordata.error !== undefined) {
if (errordata.error.constructor === Function) {
details = errordata.error()
} else {
if (errordata.error.info !== undefined) {
details = errordata.error.info
} else {
details = errordata.error
}
}
} else {
if (errordata.constructor === Function) {
details = errordata()
} else {
details = errordata
}
}
}
}
outputError(errortext, details)
}
function breakOutOfPromiseChain (error) {
return Promise.reject(error)
}
function padDateTime (n) {
return (n < 10 ? '0' + n : n)
}
function mapForObject (obj, mapFn) {
// example usage:
/*
let originalObj = { 'a': 1, 'b': 2, 'c': 3 }
let newObj = mapForObject( originalObj, ([k, v]) => ({[k]: v * v}) )
console.log(newObj) // { 'a': 1, 'b': 4, 'c': 9 }
*/
return Object.assign(...Object.entries(obj).map(mapFn))
}
class PerPageProgress {
// some functions have a certain amount of progress allotted to them.
// split this progress evenly across the pages
constructor (progressStart, progressEnd, pagecount) {
this.progressStart = progressStart
this.progressEnd = progressEnd
this.pagecount = (pagecount === undefined ? 0 : pagecount)
this.debug = false
}
increment () {
const progressPercentPerPage = (this.progressEnd - this.progressStart) / this.pagecount
let newProgress = progressbar.getProgress() + progressPercentPerPage
newProgress = Math.min(newProgress, this.progressEnd) // never increment over the target (shouldn't happen, but this is way it's failsafe)
progressbar.setProgress(newProgress)
if (this.debug) {
console.log('Incremented progressbar to ' + newProgress + ' via PerPageProgress.increment().', progressbar.getProgress())
}
}
}
/*
==============================
Specific comparison helper functions
==============================
*/
// for comparison part 0
function pingForeignWiki () {
const errortext = localizedText('cpl-error-foreignwiki', formInput.foreignWiki, `<a href="${getLocalUrl('Special:Interwiki')}">Special:Interwiki</a>`)
return new Promise((resolve, reject) => {
foreignUrl = interwikiMap[formInput.foreignWiki]
if (foreignUrl.endsWith('/wiki/$1')) {
foreignUrl = foreignUrl.slice(0, -8)
getForeignUrl = page => interwikiMap[formInput.foreignWiki].replace('$1', page) // define global function
}
const urlParams = $.param(ApiQueryParams.siteinfo())
const foreignApiUrl = foreignUrl + '/api.php?format=json&' + urlParams
$.getJSON(foreignApiUrl)
.done(siteinfo => {
if (siteinfo.query !== undefined && siteinfo.query.general !== undefined && siteinfo.query.general.base !== undefined) {
resolve(siteinfo.query.general.base)
} else {
errorDuringProcess({}, errortext)
reject(Error('siteinfo has an unexpected format!'))
}
})
.fail(errordata => {
errorDuringProcess(errordata, errortext)
reject(Error('getting JSON of siteinfo API query from foreign wiki failed! URL: ' + foreignApiUrl))
})
})
}
// for comparison part 2
function stringAboutLimitIfNecessary () {
if (pagelist.isCutOffByLimit()) {
return localizedText('cpl-log-cutoff', pagelist.rawApiResult.limits[apiLimitKeyName[formInput.pagelistMethod]])
}
return ''
}
// for comparison part 2
function loadNamespaces () {
return new Promise((resolve, reject) => {
const localNamespacesLocalized = mw.config.get('wgFormattedNamespaces')
let localNamespacesCanonical = {}
new mw.Api().get(ApiQueryParams.localNamespaces())
.done(namespacesInfo => {
if (namespacesInfo.query === undefined || namespacesInfo.query.namespaces === undefined) {
// couldn't get any info about the canonical names of local namespaces
// so just assume they are the same as the localized ones
localNamespacesCanonical = localNamespacesLocalized
} else {
for (const nsId in namespacesInfo.query.namespaces) {
const nsInfo = namespacesInfo.query.namespaces[nsId]
// fallback to localized name if canonical unavailable
localNamespacesCanonical[nsId] = nsInfo.canonical || nsInfo['*']
}
}
nsData.localNamespacesLocalized = localNamespacesLocalized
nsData.localNamespacesCanonical = localNamespacesCanonical
resolve()
})
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-canonicalns'))
reject(Error('getting local namespaces failed!'))
})
})
}
// for comparison part 4
class ValidLocalPageFilterer {
static filter () {
let filterPromise
switch (formInput.matchingMethod) {
case MatchingMethod.Iwlink:
filterPromise = ValidLocalPageFilterer._filterBasedOnIwlinks()
break
case MatchingMethod.Pagetitle:
filterPromise = ValidLocalPageFilterer._filterBasedOnPagetitle()
break
default:
filterPromise = Promise.resolve() // no filtering
break
}
return new Promise((resolve, reject) => {
filterPromise
.then(() => { resolve() })
.catch(error => { reject(error) })
})
}
static _filterBasedOnIwlinks () {
// iterate over all pages and only keep those that have an interwiki link to the foreign wiki
return new Promise((resolve, reject) => {
for (const pageid in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, pageid)) {
// we already checked langlinks in pagelist.addForeignTitles.
// pages with no langlinks got an undefined foreignTitle there
if (pagelist.pages[pageid].foreignTitle === undefined) {
delete pagelist.pages[pageid]
}
}
}
resolve()
})
}
static _filterBasedOnPagetitle () {
// make an API call on the foreign wiki with all page titles of the local wiki
return new Promise((resolve, reject) => {
// get the foreign titles of all local pages and connect them to the local page IDs
const foreignTitleToLocalPageId = {} // object to combine
const foreignTitles = []
for (const pageid in pagelist.pages) {
if (Object.hasOwnProperty.call(pagelist.pages, pageid)) {
const localPageData = pagelist.pages[pageid]
if (localPageData.foreignTitle !== undefined) {
foreignTitles.push(localPageData.foreignTitle)
foreignTitleToLocalPageId[localPageData.foreignTitle] = pageid
}
}
}
if (debug) { console.log('Foreign titles:', foreignTitles) }
// check the existence of each page, via an API request to the foreign wiki.
// the list of pages may be too long for a single API request (Error 414 URI Too Long),
// so split it up into groups, which are short enough to avoid the error when concatenated
const urilengthHardLimit = 8000 // character limit to avoid the error
// array where each element is a list of pages ("group") that will be used in the same API request
const titles = [[]]
let groupIndex = 0
// iterate over the page list
foreignTitles.forEach(foreignTitle => {
// check: has the current group reached the limit?
const urlParamTitle = $.param({ title: titles[groupIndex].join('|') })
if (urlParamTitle.length > urilengthHardLimit - 200) {
// it has, so start a new group
groupIndex++
titles.push([])
}
// add the current page to the last group
titles[groupIndex].push(foreignTitle)
})
// titles is now e.g.: [ ['page1', 'page2', 'page3'], ['page4'] ]
if (debug) { console.log('Title groups:', titles) }
const promises = []
// now make the API request for each group
titles.forEach(foreignTitles => {
const urlParams = $.param(ApiQueryParams.pagesExistence(foreignTitles.join('|')))
const foreignApiUrl = foreignUrl + '/api.php?format=json&' + urlParams
promises.push(
new Promise((resolve, reject) => {
$.getJSON(foreignApiUrl)
.done(foreignPagesExistence => {
if (debug) { console.log('Foreign API result:', foreignPagesExistence) }
let localPageIdsToDelete = []
if (foreignPagesExistence.query !== undefined && foreignPagesExistence.query.pages !== undefined) {
// iterate over the pages in the foreign API result
for (const foreignPageId in foreignPagesExistence.query.pages) {
if (foreignPageId >= 0) {
// skip pages with positive IDs (which denote existence)
continue
}
// based on the title of the current page, get the ID of the local page
const foreignTitle = foreignPagesExistence.query.pages[foreignPageId].title
const localPageId = foreignTitleToLocalPageId[foreignTitle]
// add this to the list of local page IDs that should be deleted in the pagelist
localPageIdsToDelete.push(localPageId)
}
} else {
// cannot get info about page existence on the foreign wiki,
// so assume that no pages in this group are valid
localPageIdsToDelete = foreignTitles.map(foreignTitle => foreignTitleToLocalPageId[foreignTitle])
}
resolve(localPageIdsToDelete)
})
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-foreignpages', formInput.foreignWiki))
reject(Error('getting JSON of foreign page existence API query failed! URL: ' + foreignApiUrl))
})
})
)
})
Promise.all(promises)
.then(localPageIdsToDeleteArray => {
// in the localPageIdsToDeleteArray, each element is a list of local page IDs to delete,
// from each of the API requests
localPageIdsToDeleteArray = [].concat(...localPageIdsToDeleteArray) // combine them all into one array
if (debug) { console.log('Deleting the following local page IDs from the pagelist:', localPageIdsToDeleteArray) }
localPageIdsToDeleteArray.forEach(localPageId => {
delete pagelist.pages[localPageId]
})
resolve()
})
.catch(error => { reject(error) }) // an error occurred in any of the API requests, just pass it through
})
}
}
// for comparison part 5
function getLocalDiffsizeForOnePage (parentid, revid) {
return new Promise((resolve, reject) => {
if (parentid === 0) {
// this is a new page, revid is the first revision ID of the page
resolve()
}
const apiparams = ApiQueryParams.diffSize(parentid, revid)
new mw.Api().get(apiparams)
.done(apiresult => {
let diffSize
if (apiresult.compare !== undefined && apiresult.compare.fromsize !== undefined && apiresult.compare.tosize !== undefined) {
diffSize = apiresult.compare.tosize - apiresult.compare.fromsize
}
resolve(diffSize)
})
.fail(errordata => {
resolve()
})
})
}
// for comparison part 5
function getForeignDiffsizeForOnePage (parentid, revid) {
return new Promise((resolve, reject) => {
if (parentid === 0) {
// this is a new page, revid is the first revision ID of the page
resolve()
}
const urlParams = $.param(ApiQueryParams.diffSize(parentid, revid))
const foreignApiUrl = foreignUrl + '/api.php?format=json&' + urlParams
$.getJSON(foreignApiUrl)
.done(apiresult => {
let diffSize
if (apiresult.compare !== undefined && apiresult.compare.fromsize !== undefined && apiresult.compare.tosize !== undefined) {
diffSize = apiresult.compare.tosize - apiresult.compare.fromsize
}
resolve(diffSize)
})
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-foreigndiff', formInput.foreignWiki))
reject(Error('getting JSON of foreign compare API query failed! URL: ' + foreignApiUrl))
})
})
}
// for comparison part 5
function getForeignRevsSinceCutoff (foreignPagename, latestLocalTimestamp) {
return new Promise((resolve, reject) => {
const apiparams = ApiQueryParams.revisions(latestLocalTimestamp, foreignPagename)
const foreignApiUrl = foreignUrl + '/api.php?format=json&' + $.param(apiparams)
$.getJSON(foreignApiUrl, foreignData => {
if (foreignData.query === undefined || foreignData.query.pages === undefined) {
resolve()
return
}
const foreignPageData = foreignData.query.pages[Object.keys(foreignData.query.pages)[0]]
if (foreignPageData.revisions === undefined) {
resolve()
return
}
const foreignRevs = []
foreignPageData.revisions.forEach(foreignPageRev => {
foreignRevs.push(foreignPageRev)
})
resolve(foreignRevs)
})
})
}
// for comparison part 5
function getDiffSizesForAllForeignRevsOfOnePage (foreignrevs) {
return new Promise((resolve, reject) => {
if (foreignrevs === undefined || foreignrevs.length === 0) {
resolve()
}
const allRevsWithoutLastRev = foreignrevs.slice(0, -1)
const lastrev = foreignrevs[foreignrevs.length - 1]
// we can get the diffsizes for all revs easily, because we have the array of consecutive revs
allRevsWithoutLastRev.forEach((rev, index) => {
foreignrevs[index].diffsize = foreignrevs[index].size - foreignrevs[index + 1].size
})
// we can't get it for the last rev, though, so we get that one via the API
getForeignDiffsizeForOnePage(lastrev.parentid, lastrev.revid)
.then(diffSize => {
foreignrevs[foreignrevs.length - 1].diffsize = (diffSize !== undefined ? diffSize : foreignrevs[foreignrevs.length - 1].size)
resolve(foreignrevs)
})
.catch(error => { reject(error) })
})
}
// for comparison part 7
function preparePagedataLocalForOutput (pagedata, dateformatsDate, dateformatsTime) {
const localLatestRev = pagedata.revisions[0]
const localTimestamp = new Date(localLatestRev.timestamp)
return {
timestampMillis: localTimestamp.getTime(), // timestamp in milliseconds since epoch, for sorting
revid: localLatestRev.revid,
timestamp: [
localTimestamp.getUTCFullYear(),
padDateTime(localTimestamp.getUTCMonth() + 1),
padDateTime(localTimestamp.getUTCDate()),
padDateTime(localTimestamp.getUTCHours()),
padDateTime(localTimestamp.getUTCMinutes()),
padDateTime(localTimestamp.getUTCSeconds())
].join(''), // 20210610085405
isnew: localLatestRev.parentid === 0,
isminor: localLatestRev.minor !== undefined,
timestampsDateFormatted: doFormat(dateformatsDate, localTimestamp), // 07/07/2020
timestampsTimeFormatted: doFormat(dateformatsTime, localTimestamp), // 21:15
pagename: pagedata.title,
pagelink: getLocalUrl(pagedata.title),
difflink: getLocalUrl(`${pagedata.title}?diff=${localLatestRev.revid}`),
histlink: getLocalUrl(`${pagedata.title}?action=history`),
isBigEdit: Math.abs(localLatestRev.diffsize) > 500,
diffsize: localLatestRev.diffsize,
newsize: localLatestRev.size,
userlink: getLocalUrl(`User:${localLatestRev.user}`),
username: localLatestRev.user,
usertalklink: getLocalUrl(`User_talk:${localLatestRev.user}`),
usercontribslink: getLocalUrl(`Special:Contributions/${localLatestRev.user}`),
summary: localLatestRev.parsedcomment
}
}
// for comparison part 7
function preparePagedataForeignForOutput (pagedata, dateformats, dateformatsDate, dateformatsTime) {
const foreignRevData = []
for (const foreignRevIndex in pagedata.foreignRevisions) {
if (Object.hasOwnProperty.call(pagedata.foreignRevisions, foreignRevIndex)) {
const foreignRev = pagedata.foreignRevisions[foreignRevIndex]
const revTimestamp = new Date(foreignRev.timestamp)
foreignRevData.push({
timestampMillis: revTimestamp.getTime(), // timestamp in milliseconds since epoch, for sorting
revid: foreignRev.revid,
timestamp: [
revTimestamp.getUTCFullYear(),
padDateTime(revTimestamp.getUTCMonth() + 1),
padDateTime(revTimestamp.getUTCDate()),
padDateTime(revTimestamp.getUTCHours()),
padDateTime(revTimestamp.getUTCMinutes()),
padDateTime(revTimestamp.getUTCSeconds())
].join(''), // 20210610085405
isnew: foreignRev.parentid === 0,
isminor: foreignRev.minor !== undefined,
pagename: pagedata.foreignTitle,
permalink: getForeignUrl(`${pagedata.foreignTitle}?oldid=${foreignRev.revid}`),
timestampsDateFormatted: doFormat(dateformatsDate, revTimestamp), // 07/07/2020
timestampsTimeFormatted: doFormat(dateformatsTime, revTimestamp), // 21:15
timestampsFormatted: doFormat(dateformats, revTimestamp), // 07/07/20, 21:15
curdifflink: getForeignUrl(`${pagedata.foreignTitle}?diff=0&oldid=${foreignRev.revid}`),
pagelink: getForeignUrl(pagedata.foreignTitle),
difflink: getForeignUrl(`${pagedata.foreignTitle}?diff=${foreignRev.revid}`),
histlink: getForeignUrl(`${pagedata.foreignTitle}?action=history`),
isBigEdit: Math.abs(foreignRev.diffsize) > 500,
diffsize: foreignRev.diffsize,
newsize: foreignRev.size,
userlink: getForeignUrl(`User:${foreignRev.user}`),
username: foreignRev.user,
usertalklink: getForeignUrl(`User_talk:${foreignRev.user}`),
usercontribslink: getForeignUrl(`Special:Contributions/${foreignRev.user}`),
summary: foreignRev.parsedcomment
})
}
}
const foreignInfo = {
isGroup: foreignRevData.length > 1,
revisions: foreignRevData,
firstLine: {} // data about the line that combines all revisions
}
if (foreignInfo.isGroup) {
let totalDiffSize = 0
const usersCount = {}
foreignRevData.forEach(revdata => {
totalDiffSize += revdata.diffsize
if (usersCount[revdata.username] !== undefined) {
usersCount[revdata.username]++
} else {
usersCount[revdata.username] = 1
}
})
const users = []
for (const username in usersCount) {
if (Object.hasOwnProperty.call(usersCount, username)) {
users.push({
userlink: getForeignUrl(`User:${username}`),
username: username,
userRevcount: usersCount[username]
})
}
}
foreignInfo.firstLine = {
timestamp: foreignRevData[0].timestamp, // 20210610085405
isminor: foreignRevData.every(revdata => revdata.isminor), // are all edits minor?
timestampsDateFormatted: foreignRevData[0].timestampsDateFormatted, // 07/07/2020
timestampsTimeFormatted: foreignRevData[0].timestampsTimeFormatted, // 21:15
pagelink: getForeignUrl(pagedata.foreignTitle),
pagename: pagedata.foreignTitle,
isnew: foreignRevData.some(revdata => revdata.isnew), // is any edit a new page?
difflink: getForeignUrl(`${pagedata.foreignTitle}?diff=${foreignRevData[0].revid}&oldid=${pagedata.foreignRevisions[pagedata.foreignRevisions.length - 1].parentid}`),
changesCount: foreignRevData.length,
histlink: getForeignUrl(`${pagedata.foreignTitle}?action=history`),
isBigEdit: Math.abs(totalDiffSize) > 500,
diffsize: totalDiffSize,
newsize: foreignRevData[0].newsize,
users: users
}
}
return foreignInfo
}
function doFormat (formatsObject, dataToFormat) {
return mapForObject(
formatsObject,
([lang, format]) => ({[lang]: format.format(dataToFormat)})
)
}
class FailedConfirmationError extends Error {
constructor () {
super('Process was terminated because the user did not confirm a dialog.')
}
}
/*
==============================
Display output
==============================
*/
function jQueryToString (possibleJqueryElement) {
if (possibleJqueryElement instanceof jQuery) {
return possibleJqueryElement[0].outerHTML
}
return possibleJqueryElement
}
function outputLog (logtext) {
logtext = jQueryToString(logtext)
msgWidget.setLabel(new OO.ui.HtmlSnippet(msgWidget.getLabel() + '<br/>' + logtext))
localizeAll(msgWidget.id)
}
function addToLastLog (logtext) {
logtext = jQueryToString(logtext)
msgWidget.setLabel(new OO.ui.HtmlSnippet(msgWidget.getLabel() + logtext))
localizeAll(msgWidget.id)
}
function outputError (errortext, additionaltext) {
errortext = jQueryToString(errortext)
additionaltext = jQueryToString(additionaltext)
const newMsgWidget = new OO.ui.MessageWidget({
type: 'error',
label: new OO.ui.HtmlSnippet(errortext + (additionaltext === undefined ? '' : '<br/>' + localizedText('cpl-error-details')[0].outerHTML + additionaltext))
})
$(logElement).append(newMsgWidget.$element)
localizeAll(newMsgWidget.id)
}
function outputSuccess (successtext) {
successtext = jQueryToString(successtext)
const newMsgWidget = new OO.ui.MessageWidget({
type: 'success',
label: new OO.ui.HtmlSnippet(successtext)
})
$(logElement).append(newMsgWidget.$element)
localizeAll(newMsgWidget.id)
}
function confirmationDialog (text, dialogtitle) {
text = jQueryToString(text)
dialogtitle = jQueryToString(dialogtitle)
return new Promise((resolve, reject) => {
OO.ui.confirm(text, { title: dialogtitle }).done(confirmed => {
if (confirmed) {
resolve()
} else {
reject(new FailedConfirmationError())
}
})
})
}
function loadMessagesInAllLanguages () {
const apiPromises = []
for (const lang in l10nTable) {
apiPromises.push(
new mw.Api().getMessages(msgs, { amlang: lang })
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-messages'))
reject(Error('getMessages failed for ' + lang + '!'))
})
)
}
return apiPromises
}
function loadTablesorter () {
return mw.loader.using('jquery.tablesorter')
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-sorter'))
reject(Error('loading jquery.tablesorter failed!'))
})
}
function loadMakeCollapsible () {
return mw.loader.using('jquery.makeCollapsible')
.fail(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-collapse'))
reject(Error('loading jquery.makeCollapsible failed!'))
})
}
function registerMessagesToL10n (messagesData) {
const mwL10nTable = {}
let langIndex = -1
for (const lang of Object.keys(l10nTable)) {
langIndex++
mwL10nTable[lang] = {}
for (const key in messagesData[langIndex]) {
mwL10nTable[lang]['⧼' + key + '⧽'] = messagesData[langIndex][key]
}
}
return $.i18n().load(mwL10nTable)
}
function registerTimestampsToL10n () {
return $.i18n().load(timestampsToLocalize)
}
function displayOutput () {
return new Promise((resolve, reject) => {
// load "MediaWiki:" system messages (i18n) and libraries that enables table sorting and collapsing
Promise.all([
...loadMessagesInAllLanguages(),
loadTablesorter(), // TODO: Move to gadget definition
loadMakeCollapsible() // TODO: Move to gadget definition
])
.then(data => registerMessagesToL10n(data.slice(0, -2))) // remove the last array elements, which are the promise results of loadTablesorter() and loadMakeCollapsible()
// with the loaded messages and table sortability, make the output
.then(() => OutputMaker.make(new PerPageProgress(90, 96)))
// add output to DOM in order to localize it
.then(outputNodes => {
$(outputElement)
.hide() // hide it at first, still need to localize it
.append(...outputNodes)
prepareAttributesForI18n()
return registerTimestampsToL10n()
})
.then(() => {
localizeAll()
$(outputElement).show() // display the output now
progressbar.setProgress(98)
$('.comparepagelist-makeCollapsible').makeCollapsible()
resolve()
})
.catch(errordata => {
errorDuringProcess(errordata, localizedText('cpl-error-makingoutput'))
reject(errordata)
})
})
}
class OutputMaker {
static make (perPageProgress) {
const bigWrapperTable = $('<table>')
.addClass(wrapperTableClasses)
.append(
$('<thead>').append(
$('<tr>').append(
$('<th>').html(localizedText('cpl-output-tableheadlocal')),
$('<th>').html(localizedText('cpl-output-tableheadforeign', formInput.foreignWiki))
)
)
)
const bigWrapperTBody = $('<tbody>')
bigWrapperTable.append(bigWrapperTBody)
perPageProgress.pagecount = pagelistForOutput.length
pagelistForOutput.forEach(pagedata => {
const tableRow = $('<tr>').attr('valign', 'top')
// each page row has two cells: local and foreign
// local cell
// the "expand" arrow cell is not needed for the local page, because there is only one revision in all rows there
tableRow.append(OutputMaker.cellForSingleRevision(pagedata.local, true))
// foreign cell
if (pagedata.foreign.isGroup) {
// there are multiple revisions to display
tableRow.append(OutputMaker.cellForRevisionGroup(pagedata.foreign))
} else {
// there is only one revision to display
tableRow.append(OutputMaker.cellForSingleRevision(pagedata.foreign.revisions[0], false))
}
perPageProgress.increment()
bigWrapperTBody.append(tableRow)
})
bigWrapperTable.tablesorter() // make the table sortable
const note = $('<span>')
.addClass(noteClasses)
.html(localizedText('cpl-output-timezone'))
return [bigWrapperTable, note]
}
static expandArrow (hideCell, showArrow) {
const expandArrowClasses = ['mw-enhancedchanges-arrow-space']
if (showArrow) {
expandArrowClasses.push(
'mw-collapsible-toggle',
'mw-collapsible-arrow',
'mw-enhancedchanges-arrow',
'mw-collapsible-toggle-collapsed'
)
}
const cell = $('<td>')
.append(
$('<span>')
.addClass(expandArrowClasses)
.attr('tabIndex', 0)
)
if (hideCell) {
cell.hide()
}
return cell
}
static flagsCell (isnew, isminor) {
let flagNew, flagMinor
if (isnew) {
flagNew = $('<abbr>')
.addClass('newpage')
.html(localizedText('newpageletter')) // 'N'
.attr('title', localizedAttribute('title', 'recentchanges-label-newpage')) // 'This edit created a new page'
} else {
flagNew = nbsp()
}
if (isminor) {
flagMinor = $('<abbr>')
.addClass('minoredit')
.html(localizedText('minoreditletter')) // 'm'
.attr('title', localizedAttribute('title', 'recentchanges-label-minor')) // 'This is a minor edit'
} else {
flagMinor = nbsp()
}
const cell = $('<td>')
.addClass('mw-enhanced-rc')
.append(flagNew, flagMinor, nbsp(), nbsp())
return cell
}
static flagsAndTimestamp (isnew, isminor, datesFormatted, timesFormatted) {
const cell = OutputMaker.flagsCell(isnew, isminor)[0]
// remove the last nbsp in the flags cell
cell.childNodes[cell.childNodes.length - 1].remove()
$(cell).append(
localizedTimestamp(datesFormatted),
nbsp(),
br(),
nbsp(), nbsp(), nbsp(),
localizedTimestamp(timesFormatted),
nbsp()
)
return cell
}
static articleLink (pagelink, pagename, noWrapper) {
const articleLink = $('<span>')
.addClass('mw-title')
.append(
$('<a>')
.addClass(['mw-changeslist-title', 'extiw'])
.text(pagename)
.attr({
href: pagelink,
title: pagename
})
)
if (noWrapper) {
return articleLink
}
const articleLinkWrapper = $('<span>')
.addClass('mw-changeslist-line-inner-articleLink')
.append(' ', articleLink)
return articleLinkWrapper
}
static revisionLink (permalink, pagename, timestampsFormatted) {
const revisionLink = $('<span>')
.addClass('mw-enhanced-rc-time')
.append(
$('<a>')
.html(localizedTimestamp(timestampsFormatted))
.attr({
href: permalink,
title: pagename
})
)
return revisionLink
}
static historyLinks (isnew, difflink, histlink, pagename) {
// diff link
const diffLink = $('<span>')
const diffText = localizedText('diff') // 'diff'
if (isnew) {
// new page, no link necessary
diffLink.html(diffText)
} else {
diffLink.append($('<a>')
.addClass(['mw-changeslist-diff', 'extiw'])
.html(diffText)
.attr('href', difflink)
)
}
// hist link
const histLink = $('<span>')
.append(
$('<a>')
.addClass('mw-changeslist-history')
.html(localizedText('hist')) // 'hist'
.attr({
href: histlink,
title: pagename
})
)
// diff link and hist link combined
const historyLinks = $('<span>')
.addClass('mw-changeslist-line-inner-historyLink')
.append(
' ',
$('<span>')
.addClass('mw-changeslist-links')
.append(diffLink, histLink)
)
return historyLinks
}
static curPrevLinks (curdifflink, isnew, difflink, pagename) {
// cur link
const curLink = $('<a>')
.addClass('mw-changeslist-diff-cur')
.html(localizedText('cur')) // 'cur'
.attr('href', curdifflink)
// prev link
const prevText = localizedText('last') // 'prev'
let prevLink
if (isnew) {
prevLink = prevText
} else {
prevLink = $('<a>')
.addClass('mw-changeslist-diff')
.html(prevText)
.attr({
href: difflink,
title: pagename
})
prevLink = prevLink[0].outerHTML
}
const textInParen = localizedTextWithReparse('textInParen', () => {
return localizedTextRaw('parentheses').replace('$1',
curLink[0].outerHTML +
localizedTextRaw('pipe-separator') +
prevLink
)
})
const separator = $('<span>').addClass('mw-changeslist-separator')
const parsedhtml = $.parseHTML(
' ' +
textInParen +
' ' +
separator[0].outerHTML +
' '
)
return parsedhtml
}
static groupHistoryLinks (isnew, difflink, pagename, changesCount, histlink) {
// changes link
const changesLinkWrapper = $('<span>')
const changesLinkText = localizedText('nchanges', changesCount)
if (isnew) {
changesLinkWrapper.append(changesLinkText)
} else {
changesLinkWrapper.append(
$('<a>')
.addClass('mw-changeslist-groupdiff')
.html(changesLinkText)
.attr({
href: difflink,
title: pagename
})
)
}
// history link
const historyLink = $('<a>')
.addClass('mw-changeslist-history')
.html(localizedText('enhancedrc-history')) // 'history'
.attr({
href: histlink,
title: pagename
})
// changes link and history link combined
const groupHistoryLinks = $('<span>')
.addClass('mw-changeslist-links')
.append(
changesLinkWrapper,
' ',
$('<span>').append(historyLink)
)
return groupHistoryLinks
}
static separator (classname) {
const separatorWrapper = $('<span>')
.addClass(classname)
.append(
' ',
$('<span>').addClass('mw-changeslist-separator'),
' '
)
return separatorWrapper
}
static characterDiff (isBigEdit, diffsize, newsize, noWrapper) {
const characterDiff = $(isBigEdit ? '<strong>' : '<span>')
.addClass('mw-diff-bytes')
.html(localizedNumber(diffsize, 'withsign'))
.attr({
dir: 'ltr',
// the newsize number is not localized here because localization would get too convoluted:
// a localized number that is the parameter for a localization of an attribute
title: localizedAttribute('title', 'rc-change-size-new', newsize)
})
let diffClass = 'mw-plusminus-'
diffClass += (diffsize === 0 ? 'null' : (diffsize > 0 ? 'pos' : 'neg'))
characterDiff.addClass(diffClass)
if (noWrapper) {
return characterDiff
}
const characterDiffWrapper = $('<span>')
.addClass('mw-changeslist-line-inner-characterDiff')
.append(characterDiff)
return characterDiffWrapper
}
static userLink (userlink, username, noWrapper) {
const userLink = $('<a>')
.addClass(['mw-userlink', 'userlink'])
.attr({
href: userlink,
title: `${nsData.localNamespacesLocalized[2]}:${username}`
})
.append(
$('<bdi>').text(username)
)
if (noWrapper) {
return userLink
}
const userLinkWrapper = $('<span>')
.addClass('mw-changeslist-line-inner-userLink')
.append(userLink)
return userLinkWrapper
}
static groupUsers (users) {
const groupUsersArray = []
users.forEach(userinfo => {
const userLink = $('<a>')
.addClass(['mw-userlink', 'userlink'])
.attr({
href: userinfo.userlink,
title: `${nsData.localNamespacesLocalized[2]}:${userinfo.username}`
})
.append(
$('<bdi>').text(userinfo.username)
)
let groupUser = userLink[0].outerHTML
if (userinfo.userRevcount > 1) {
groupUser +=
' ' +
localizedTextWithReparse('userNtimes', () => {
return localizedTextRaw('parentheses',
localizedTextRaw('ntimes', userinfo.userRevcount)
)
})
}
groupUsersArray.push(groupUser)
})
const textInBrackets = localizedTextWithReparse('textInBrackets', () => {
return localizedTextRaw('brackets').replace('$1',
groupUsersArray.join(localizedTextRaw('semicolon-separator'))
)
})
const groupUsersWrapper = $('<span>')
.addClass('changedby')
.append($.parseHTML(textInBrackets))
return groupUsersWrapper
}
static userToolLinks (username, usertalklink, usercontribslink, noWrapper) {
// user talk link
const talkLink = $('<span>')
.append(
$('<a>')
.addClass('mw-usertoollinks-talk')
.html(localizedText('talkpagelinktext'))
.attr({
href: usertalklink,
title: `${nsData.localNamespacesLocalized[3]}:${username}` // TODO: localize
})
)
// user contribs link
const userContribsLinkWrapper = $('<span>')
.append(
$('<a>')
.addClass('mw-usertoollinks-contribs')
.html(localizedText('contribslink'))
.attr({
href: usercontribslink,
title: `Special:Contributions/${username}` // TODO: localize
})
)
// user talk link and contribs link combined
const userToolLinks = $('<span>')
.addClass(['mw-usertoollinks', 'mw-changeslist-links'])
.append(talkLink, ' ', userContribsLinkWrapper)
if (noWrapper) {
return userToolLinks
}
const userToolLinksWrapper = $('<span>')
.addClass('mw-changeslist-line-inner-userTalkLink')
.append(' ', userToolLinks)
return userToolLinksWrapper
}
static comment (summary) {
const comment = $('<span>')
.addClass('mw-changeslist-line-inner-comment')
if (summary !== '') {
comment.append(
' ',
$('<span>')
.addClass(['comment', 'comment--without-parentheses'])
.append(
$('<span>')
.html(summary)
.attr({
dir: 'auto'
})
)
)
}
return comment
}
static commentSimple (summary) {
if (summary) {
const comment = $('<span>')
.addClass(['comment', 'comment--without-parentheses'])
.html(summary)
return comment
}
}
static cellForSingleRevision (revdata, hideExpandArrowCell) {
// ====== First column ======
const firstColumn = OutputMaker.expandArrow(hideExpandArrowCell)
// ====== Second column ======
// flags and timestamp
const secondColumn = OutputMaker.flagsAndTimestamp(
revdata.isnew, revdata.isminor,
revdata.timestampsDateFormatted, revdata.timestampsTimeFormatted
)
// ====== Third column ======
// all other information about the page
const thirdColumn = $('<td>')
.addClass('mw-changeslist-line-inner')
.attr('data-target-page', revdata.pagename)
thirdColumn.append(
// article link
OutputMaker.articleLink(revdata.pagelink, revdata.pagename),
// history links
OutputMaker.historyLinks(
revdata.isnew, revdata.difflink,
revdata.histlink, revdata.pagename
),
// separator after links
OutputMaker.separator('mw-changeslist-line-inner-separatorAfterLinks'),
// character diff
OutputMaker.characterDiff(
revdata.isBigEdit, revdata.diffsize,
revdata.newsize
),
// separator after character diff
OutputMaker.separator('mw-changeslist-line-inner-separatorAftercharacterDiff'),
// user link
OutputMaker.userLink(revdata.userlink, revdata.username),
// user talk and contribs links
OutputMaker.userToolLinks(revdata.username, revdata.usertalklink, revdata.usercontribslink),
// edit summary
OutputMaker.comment(revdata.summary)
)
const tableInCell = $('<table>')
.addClass([
'mw-enhanced-rc',
'mw-changeslist-line',
'mw-changeslist-edit'
])
.attr({
'data-mw-revid': revdata.revid,
'data-mw-ts': revdata.timestamp
})
const cell = $('<td>')
.append(
tableInCell.append(
$('<tbody>').append(
$('<tr>').append(firstColumn, secondColumn, thirdColumn)
)
)
)
.css('vertical-align', 'top')
.attr('data-sort-value', revdata.timestamp)
return cell
}
static firstRowOfRevisionGroup (revgroupdata) {
// ====== First column ======
const firstColumn = OutputMaker.expandArrow(false, true)
// ====== Second column ======
// flags and timestamp
const secondColumn = OutputMaker.flagsAndTimestamp(
revgroupdata.isnew, revgroupdata.isminor,
revgroupdata.timestampsDateFormatted, revgroupdata.timestampsTimeFormatted
)
// ====== Third column ======
// all other information about the page
const thirdColumn = $('<td>')
.addClass('mw-changeslist-line-inner')
.attr('data-target-page', revgroupdata.pagename)
thirdColumn.append(
// article link
OutputMaker.articleLink(revgroupdata.pagelink, revgroupdata.pagename, true),
' ',
// history links
OutputMaker.groupHistoryLinks(
revgroupdata.isnew, revgroupdata.difflink,
revgroupdata.pagename, revgroupdata.changesCount,
revgroupdata.histlink
),
// separator after links
$('<span>').addClass('mw-changeslist-separator'),
// character diff
OutputMaker.characterDiff(
revgroupdata.isBigEdit, revgroupdata.diffsize,
revgroupdata.newsize
),
// separator after character diff
' ',
$('<span>').addClass('mw-changeslist-separator'),
// user link
OutputMaker.userLink(revgroupdata.userlink, revgroupdata.username),
// user talk and contribs links
OutputMaker.groupUsers(revgroupdata.users)
)
const row = $('<tr>').append(firstColumn, secondColumn, thirdColumn)
return row
}
static rowOfRevisionGroup (revdata) {
// ====== First column ======
// the "expand" arrow
const firstColumn = OutputMaker.expandArrow(false)
// ====== Second column ======
// flags and timestamp
const secondColumn = OutputMaker.flagsCell(revdata.isnew, revdata.isminor)
// ====== Third column ======
// all other information about the page
const thirdColumn = $('<td>')
.addClass('mw-enhanced-rc-nested')
.attr('data-target-page', revdata.pagename)
thirdColumn.append(
// revision link
OutputMaker.revisionLink(
revdata.permalink, revdata.pagename,
revdata.timestampsFormatted
),
// cur-prev links
OutputMaker.curPrevLinks(
revdata.curdifflink, revdata.isnew,
revdata.difflink, revdata.pagename
),
// character diff
OutputMaker.characterDiff(
revdata.isBigEdit, revdata.diffsize,
revdata.newsize, true
),
// separator after character diff
' ',
$('<span>').addClass('mw-changeslist-separator'),
' ',
// user link
OutputMaker.userLink(revdata.userlink, revdata.username, true),
' ',
// user talk and contribs links
OutputMaker.userToolLinks(
revdata.username, revdata.usertalklink,
revdata.usercontribslink, true
),
' ',
// edit summary
OutputMaker.commentSimple(revdata.summary)
)
const row = $('<tr>')
.addClass([
'mw-enhanced-rc',
'mw-changeslist-line',
'mw-changeslist-edit'
])
.attr({
'data-mw-revid': revdata.revid,
'data-mw-ts': revdata.timestamp
})
.append(firstColumn, secondColumn, thirdColumn)
return row
}
static cellForRevisionGroup (foreignpagedata) {
const tableInCell = $('<table>')
.addClass([
'comparepagelist-makeCollapsible',
'mw-collapsed',
'mw-enhanced-rc',
'mw-changeslist-line',
'mw-changeslist-edit'
])
.attr('data-mw-ts', foreignpagedata.firstLine.timestamp)
const tbody = $('<tbody>')
.append(OutputMaker.firstRowOfRevisionGroup(foreignpagedata.firstLine))
foreignpagedata.revisions.forEach(foreignRev => {
tbody.append(OutputMaker.rowOfRevisionGroup(foreignRev))
})
const cell = $('<td>')
.append(tableInCell.append(tbody))
.css('vertical-align', 'top')
.attr('data-sort-value', foreignpagedata.firstLine.timestamp)
return cell
}
}
const nbsp = () => document.createTextNode(String.fromCodePoint(0x00A0))
const br = () => document.createElement('br')
const cplVersion = '3'
/*
==============================
Wiki-specific code
==============================
*/
// CSS classes for the output table
const wrapperTableClasses = ['terraria', 'lined']
// CSS classes for an element to make it appear as a less important note
const noteClasses = ['note-text']