Terraria Wiki
Advertisement
Terraria Wiki

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

==============================
Internationalization
==============================

*/

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-error-radiomissing': 'Field is required! Please select an option.',
    'cpl-input-error-textinputmissing': 'Field is required! Please enter a value.',

    '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-messages': 'Couldn\'t load strings for the recent changes-style 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-error-radiomissing': 'Erforderliches Feld! Wähle eine Option aus.',
    'cpl-input-error-textinputmissing': 'Erforderliches Feld! Bitte gib einen Wert 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-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?'
  }
}

// system messages used in the output
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'
]

/*

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

/*

==============================
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 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 []
    }

    const cateList = []
    apiResult.query.allcategories.forEach(cateObj => {
      cateList.push(cateObj['*'])
    })
    return cateList
  }

  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
$(document).ready(() => { $('.client-nojs').hide() })

// load dependencies
Promise.all([
  // load OOUI
  new Promise((resolve, reject) => {
    mw.loader.using('oojs-ui').done(() => { resolve() })
  }),
  // load jquery.i18n and initialize the l10n table
  new Promise((resolve, reject) => {
    mw.loader.using('jquery.i18n').done(() => {
      $.i18n().load(l10nTable).done(() => {
        resolve()
      })
    })
  }),
  // load the category and interwiki lists
  new Promise((resolve, reject) => {
    new InputFormPreparer().makeCateAndIwList().then(data => {
      console.log('[Compare page list] Category and interwiki list on this wiki:', data)
      resolve(data)
    })
  })
]).then(data => {
  // when done, start making the input form
  // (data is an array of the results of the promises above)
  makeInputsAndAddThemToPage(data[2].cateList, data[2].iwList)
})

/*

==============================
Making the input form
==============================

*/

// entry function
function makeInputsAndAddThemToPage (cateList, iwList) {
  InputHelper.loadCSS()

  /*
  * Import source of MessageWidget.
  * The built-in MessageWidget requires OOUI >= v0.33.0. Sources:
  * - https://phabricator.wikimedia.org/T225282#5290991
  * - https://github.com/wikimedia/oojs-ui/commit/68a3f1c91f4fc6a501bb28880a7086d221f171e4
  * Fandom has OOUI v0.6.0, though, at least according to https://github.com/Wikia/app/blob/dev/resources/lib/oojs-ui/oojs-ui.js
  */
  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) } // eslint-disable-line

  // make fieldsets
  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-inputarea').append(
    pagelistFieldset.fieldset.$element,
    foreignwikiFieldset.fieldset.$element,
    debugButtonFieldset.$element,
    new OO.ui.FieldsetLayout({ items: [submitButton] }).$element
  )

  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
    const modules = [
      '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'
    ]
    mw.loader.load(modules, 'text/css')

    const urlParams = {
      only: 'styles',
      modules: modules.join('|'),
      lang: mw.config.get('wgUserLanguage'),
      skin: mw.config.get('skin')
    }
    const url = mw.config.get('wgServer') + mw.config.get('wgLoadScript') + '?' + $.param(urlParams)
    console.log('[Compare page list] Loaded modules:', url)

    // load custom CSS for the Hydra and HydraDark skins
    const skin = mw.config.get('skin')
    if (skin === 'hydra' || skin === 'hydradark') {
      const cssText = '.oo-ui-icon-infoFilled{-webkit-mask:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%3E%3Ctitle%3Einfo%3C%2Ftitle%3E%3Cpath%20d%3D%22M10%200C4.477%200%200%204.477%200%2010s4.477%2010%2010%2010%2010-4.477%2010-10S15.523%200%2010%200zM9%205h2v2H9zm0%204h2v6H9z%22%2F%3E%3C%2Fsvg%3E) no-repeat center;mask:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%3E%3Ctitle%3Einfo%3C%2Ftitle%3E%3Cpath%20d%3D%22M10%200C4.477%200%200%204.477%200%2010s4.477%2010%2010%2010%2010-4.477%2010-10S15.523%200%2010%200zM9%205h2v2H9zm0%204h2v6H9z%22%2F%3E%3C%2Fsvg%3E) no-repeat center}.oo-ui-messageWidget{position:relative;-moz-box-sizing:border-box;box-sizing:border-box;font-weight:700}.oo-ui-messageWidget>.oo-ui-labelElement-label{display:block}.oo-ui-messageWidget>.oo-ui-iconElement-icon{-webkit-mask-position:0 0;mask-position:0 0}.oo-ui-messageWidget>.oo-ui-labelElement-label{margin-left:2em}.oo-ui-messageWidget.oo-ui-messageWidget-block{border:1px solid;padding:16px 24px;font-weight:400}.oo-ui-messageWidget.oo-ui-messageWidget-block>.oo-ui-iconElement-icon{-webkit-mask-position:0 16px;mask-position:0 16px}.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-error{background-color:#fee7e6;border-color:#d33}.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-warning{background-color:#fef6e7;border-color:#fc3}.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-success{background-color:#d5fdf4;border-color:#14866d}.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-notice{background-color:#eaecf0;border-color:#a2a9b1}.oo-ui-messageWidget.oo-ui-flaggedElement-error:not(.oo-ui-messageWidget-block){color:#d33}.oo-ui-messageWidget.oo-ui-flaggedElement-success:not(.oo-ui-messageWidget-block){color:#14866d}.oo-ui-messageWidget+.oo-ui-messageWidget{margin-top:8px}.oo-ui-messageWidget.oo-ui-flaggedElement-error>.oo-ui-iconElement-icon{background-color:#d33}.oo-ui-messageWidget.oo-ui-flaggedElement-warning>.oo-ui-iconElement-icon{background-color:#fc3}.oo-ui-messageWidget.oo-ui-flaggedElement-success>.oo-ui-iconElement-icon{background-color:#14866d;background-image:none}'
      mw.util.addCSS(cssText)
    }
  }

  // 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 makeDebugButtonFieldset () {
    return new OO.ui.FieldsetLayout({
      items: [
        new OO.ui.ToggleButtonWidget({
          label: $.i18n('cpl-input-submit-debug'),
          invisibleLabel: true,
          title: $.i18n('cpl-input-submit-debug'),
          icon: 'robot'
        })
      ]
    })
  }

  static makeSubmitButton () {
    return new OO.ui.ButtonWidget({
      label: $.i18n('cpl-input-submit-label'),
      title: $.i18n('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() // 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: $.i18n('cpl-input-pagelist-cateplaceholder') }, 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: $.i18n('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' ? $.i18n('cpl-mainspace') : data[nsIndex])
        })
      }
    }
    this.nsInput = new OO.ui.DropdownInputWidget({ options: nsOptions })

    this.nsInputWrapper = new OO.ui.HorizontalLayout({
      items: [
        new OO.ui.LabelWidget({ label: $.i18n('cpl-input-pagelist-nslabel') }),
        this.nsInput
      ]
    })
    this.nsInputWrapper.toggle(false) // hide initially
  }

  validateSinglePageInput (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: $.i18n('cpl-input-pagelist-singleplaceholder'),
      validate: pagename => this.validateSinglePageInput(pagename)
    })

    this.singlePageInputWrapper = new OO.ui.HorizontalLayout({
      items: [
        new OO.ui.LabelWidget({ label: $.i18n('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 () {
    const radioButtonCate = new OO.ui.RadioOptionWidget({
      data: PagelistMethod.Cate,
      label: $.i18n('cpl-input-pagelist-radiocate')
    })
    const radioButtonNs = new OO.ui.RadioOptionWidget({
      data: PagelistMethod.Ns,
      label: $.i18n('cpl-input-pagelist-radions')
    })
    const radioButtonSinglePage = new OO.ui.RadioOptionWidget({
      data: PagelistMethod.SinglePage,
      label: $.i18n('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() ? 'non-empty' : 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 => this.validateSinglePageInput(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.fieldset = new OO.ui.FieldsetLayout({
      label: $.i18n('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')).text($.i18n('cpl-input-foreign-iwlabel'))
        }),
        this.iwInput
      ]
    })
  }

  makeRadioButtons () {
    const radioButtonIwlink = new OO.ui.RadioOptionWidget({
      data: MatchingMethod.Iwlink,
      label: $.i18n('cpl-input-foreign-radioiw')
    })
    const radioButtonPagetitle = new OO.ui.RadioOptionWidget({
      data: MatchingMethod.Pagetitle,
      label: $.i18n('cpl-input-foreign-radiotitle')
    })

    this.radiobuttons = new OO.ui.RadioSelectWidget({
      items: [radioButtonIwlink, radioButtonPagetitle]
    })
  }

  combineAll () {
    const helptext = '' +
      `<p>${$.i18n('cpl-input-foreign-helpintro')}</p>` +
      `<p>${$.i18n('cpl-input-foreign-helpiw')}</p>` +
      `<p>${$.i18n('cpl-input-foreign-helptitle')}</p>`

    this.fieldlayout = new OO.ui.FieldLayout(this.radiobuttons, {
      label: $.i18n('cpl-input-foreign-radiolabel'),
      help: new OO.ui.HtmlSnippet(helptext),
      align: 'inline'
    })

    this.fieldset = new OO.ui.FieldsetLayout({
      label: $.i18n('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 onSubmit (pagelistFieldset, foreignwikiFieldset, debugButtonFieldset) {
  // display error messages if the radio buttons are not selected
  const errortext = $.i18n('cpl-input-error-radiomissing')
  pagelistFieldset.fieldlayout.setErrors((pagelistFieldset.pagelistInput.method === undefined ? [errortext] : []))
  foreignwikiFieldset.fieldlayout.setErrors((foreignwikiFieldset.foreignwikiInput.matchmethod === undefined ? [errortext] : []))

  // make an additional check if pagelistMethod is "Cate" or "SinglePage"
  // to see if the input is valid
  new Promise((resolve, reject) => {
    switch (pagelistFieldset.pagelistInput.method) {
      case PagelistMethod.Cate:
        pagelistFieldset.cateInput.getValidity()
          .fail(() => {
            pagelistFieldset.cateInput.setValidityFlag(false)
            pagelistFieldset.fieldlayout.setErrors([$.i18n('cpl-input-error-textinputmissing')])
            pagelistFieldset.cateInput.scrollElementIntoView()
            reject()
          })
          .done(() => { resolve() })
        break

      case PagelistMethod.SinglePage:
        pagelistFieldset.singlePageInput.getValidity()
          .fail(() => {
            pagelistFieldset.singlePageInput.setValidityFlag(false)
            pagelistFieldset.fieldlayout.setErrors([$.i18n('cpl-input-error-textinputmissing')])
            pagelistFieldset.singlePageInput.scrollElementIntoView()
            reject()
          })
          .done(() => { resolve() })
        break

      default:
        // the pagelistMethod that was selected does not require additional validation
        resolve()
        break
    }
  }).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) {
  // intialize 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: new OO.ui.HtmlSnippet($.i18n('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($.i18n('cpl-log-proc-endsuccess')))
    .catch(errReason => outputLog($.i18n(errReason)))
}

// 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($.i18n('cpl-log-foreignwiki', formInput.foreignWiki, `<a href="${foreignMainpage}">${foreignMainpage}</a>`))

          outputLog($.i18n((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($.i18n((formInput.pagelistMethod === PagelistMethod.SinglePage ? 'cpl-log-fetchingpage-done' : 'cpl-log-fetchingpages-done')))

          if (pagelist.localPagesCount <= 0) {
            switch (formInput.pagelistMethod) {
              case PagelistMethod.Cate:
                addToLastLog($.i18n('cpl-log-emptycate'))
                break
              case PagelistMethod.Ns:
                addToLastLog($.i18n('cpl-log-emptyns'))
                break
              default:
                break
            }
            return breakOutOfPromiseChain()
          }

          switch (formInput.pagelistMethod) {
            case PagelistMethod.Cate:
              addToLastLog($.i18n('cpl-log-foundpagescate', pagelist.localPagesCount))
              break
            case PagelistMethod.Ns:
              addToLastLog($.i18n('cpl-log-foundpagesns', pagelist.localPagesCount))
              break
            case PagelistMethod.SinglePage:
              addToLastLog($.i18n('cpl-log-foundpage'))
              break
            default:
              break
          }
          addToLastLog(stringAboutLimitIfNecessary())

          progressbar.setProgress(25)

          switch (formInput.matchingMethod) {
            case MatchingMethod.Iwlink:
              outputLog($.i18n('cpl-log-removeinvalidiw', formInput.foreignWiki))
              break
            case MatchingMethod.Pagetitle:
              outputLog($.i18n('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($.i18n('cpl-output-warningdialog-text'), $.i18n('cpl-output-warningdialog-title'))
          }

          return canContinue.then(() => { return 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($.i18n('cpl-log-removeinvalidiw-done'))
              break
            case PagelistMethod.Ns:
              addToLastLog($.i18n('cpl-log-removeinvalidtitle-done'))
              break
            default:
              break
          }

          if (pagelist.localPagesCount <= 0) {
            addToLastLog($.i18n('cpl-log-remainingpages', 0))
            return breakOutOfPromiseChain()
          }

          addToLastLog($.i18n('cpl-log-remainingpages', pagelist.localPagesCount))
          outputLog($.i18n('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($.i18n('cpl-log-fetchingrevs-done'))
          if (pagelist.localPagesCount <= 0) {
            addToLastLog($.i18n('cpl-log-outdatedpages', 0))
            if (formInput.pagelistMethod !== PagelistMethod.SinglePage) {
              outputSuccess($.i18n('cpl-output-nothingtodisplay'))
            } else {
              outputSuccess($.i18n('cpl-output-nothingtodisplay-single', formInput.SinglePage))
            }
            return breakOutOfPromiseChain()
          }

          addToLastLog($.i18n('cpl-log-outdatedpages', pagelist.localPagesCount))
          outputLog($.i18n('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($.i18n('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.log(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.`, $.i18n('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 = Math.random() * 1500 + 1000 // 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) {
    const dateformat = new Intl.DateTimeFormat(userLanguage, {
      timeZone: 'UTC',
      hourCycle: 'h23',
      dateStyle: 'short',
      timeStyle: 'short'
    })

    const dateformatDate = new Intl.DateTimeFormat(userLanguage, {
      timeZone: 'UTC',
      dateStyle: 'short'
    })

    const dateformatTime = new Intl.DateTimeFormat(userLanguage, {
      timeZone: 'UTC',
      hourCycle: 'h23',
      hour: '2-digit',
      minute: '2-digit'
    })

    const numberformatForDiffsize = new Intl.NumberFormat(userLanguage, {
      signDisplay: 'exceptZero'
    })

    const numberformatForNewsize = new Intl.NumberFormat(userLanguage)

    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, dateformatDate, dateformatTime, numberformatForDiffsize, numberformatForNewsize),
          foreign: preparePagedataForeignForOutput(pagedata, dateformat, dateformatDate, dateformatTime, numberformatForDiffsize, numberformatForNewsize)
        }

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

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 = $.i18n('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 $.i18n('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, $.i18n('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, $.i18n('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, $.i18n('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, dateformatDate, dateformatTime, numberformatForDiffsize, numberformatForNewsize) {
  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,
    timestampDateFormatted: dateformatDate.format(localTimestamp), // 07/07/2020
    timestampTimeFormatted: dateformatTime.format(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,
    newsizeFormatted: numberformatForNewsize.format(localLatestRev.size),
    diffsizeFormatted: numberformatForDiffsize.format(localLatestRev.diffsize),
    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, dateformat, dateformatDate, dateformatTime, numberformatForDiffsize, numberformatForNewsize) {
  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}`),
        timestampDateFormatted: dateformatDate.format(revTimestamp), // 07/07/2020
        timestampTimeFormatted: dateformatTime.format(revTimestamp), // 21:15
        timestampFormatted: dateformat.format(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,
        newsizeFormatted: numberformatForNewsize.format(foreignRev.size),
        diffsizeFormatted: numberformatForDiffsize.format(foreignRev.diffsize),
        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?
      timestampDateFormatted: foreignRevData[0].timestampDateFormatted, // 07/07/2020
      timestampTimeFormatted: foreignRevData[0].timestampTimeFormatted, // 21:15
      timestampFormatted: foreignRevData[0].timestampFormatted, // 07/07/20, 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,
      newsizeFormatted: foreignRevData[0].newsizeFormatted,
      diffsizeFormatted: numberformatForDiffsize.format(totalDiffSize),
      users: users
    }
  }

  return foreignInfo
}

class FailedConfirmationError extends Error {
  constructor () {
    super('Process was terminated because the user did not confirm a dialog.')
  }
}

/*

==============================
Display output
==============================

*/

function outputLog (logtext) {
  msgWidget.setLabel(new OO.ui.HtmlSnippet(msgWidget.getLabel() + '<br/>' + logtext))
}

function addToLastLog (logtext) {
  msgWidget.setLabel(new OO.ui.HtmlSnippet(msgWidget.getLabel() + logtext))
}

function outputError (errortext, additionaltext) {
  $(logElement).append(new OO.ui.MessageWidget({
    type: 'error',
    label: new OO.ui.HtmlSnippet(errortext + (additionaltext === undefined ? '' : '<br/>' + $.i18n('cpl-error-details') + additionaltext))
  }).$element)
}

function outputSuccess (successtext) {
  $(logElement).append(new OO.ui.MessageWidget({
    type: 'success',
    label: new OO.ui.HtmlSnippet(successtext)
  }).$element)
}

function confirmationDialog (text, dialogtitle) {
  return new Promise((resolve, reject) => {
    OO.ui.confirm(text, { title: dialogtitle }).done(confirmed => {
      if (confirmed) {
        resolve()
      } else {
        reject(new FailedConfirmationError())
      }
    })
  })
}

function displayOutput () {
  return new Promise((resolve, reject) => {
    let outputNodes

    // i18n: load "MediaWiki:" system messages
    new mw.Api().loadMessagesIfMissing(msgs)
      .done(() => {
        // with the loaded messages, make the output
        outputNodes = OutputMaker.make(new PerPageProgress(90, 96))

        // load the library that enables table collapsing
        mw.loader.using('jquery.makeCollapsible')
          .done(() => {
            // once loaded, make the output visible
            $(outputElement).append(...outputNodes)
            progressbar.setProgress(98)
            $('.comparepagelist-makeCollapsible').makeCollapsible()
            resolve()
          })
          .fail(errordata => {
            errorDuringProcess(errordata, $.i18n('cpl-error-collapse'))
            reject(Error('loading jquery.makeCollapsible failed!'))
          })
      })
      .fail(errordata => {
        errorDuringProcess(errordata, $.i18n('cpl-error-messages'))
        reject(Error('loadMessagesIfMissing failed!'))
      })
  })
}

class OutputMaker {
  static make (perPageProgress) {
    const bigTerrariaTable = $('<table>')
      .addClass(['terraria', 'lined'])
      .append(
        $('<tr>').append(
          $('<th>').html($.i18n('cpl-output-tableheadlocal')),
          $('<th>').html($.i18n('cpl-output-tableheadforeign', formInput.foreignWiki))
        )
      )

    perPageProgress.pagecount = pagelistForOutput.length
    pagelistForOutput.forEach(pagedata => {
      const tableRow = $('<tr>').attr('valign', 'top')

      // 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) {
        tableRow.append(OutputMaker.cellForRevisionGroup(pagedata.foreign))
      } else {
        tableRow.append(OutputMaker.cellForSingleRevision(pagedata.foreign.revisions[0], false))
      }

      perPageProgress.increment()

      bigTerrariaTable.append(tableRow)
    })

    const note = $('<span>')
      .addClass('note-text')
      .text($.i18n('cpl-output-timezone'))

    return [bigTerrariaTable, 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) {
    const flagNew = $('<abbr>')
      .addClass('newpage')
      .text(mw.msg('newpageletter')) // 'N'
      .attr('title', mw.msg('recentchanges-label-newpage')) // 'This edit created a new page'

    const flagMinor = $('<abbr>')
      .addClass('minoredit')
      .text(mw.msg('minoreditletter')) // 'm'
      .attr('title', mw.msg('recentchanges-label-minor')) // 'This is a minor edit'

    const cell = $('<td>')
      .addClass('mw-enhanced-rc')
      .append(
        (isnew ? flagNew : nbsp()),
        (isminor ? flagMinor : nbsp()),
        nbsp(), nbsp()
      )

    return cell
  }

  static flagsAndTimestamp (isnew, isminor, dateFormatted, timeFormatted) {
    const cell = OutputMaker.flagsCell(isnew, isminor)[0]

    // remove the last nbsp in the flags cell
    cell.childNodes[cell.childNodes.length - 1].remove()

    cell.append(
      dateFormatted,
      nbsp(),
      br(),
      nbsp(), nbsp(), nbsp(),
      timeFormatted,
      nbsp()
    )

    return cell
  }

  static flagsShort (isnew, isminor) {
    const [flagNew, flagMinor] = OutputMaker.flagsCell()

    const cell = $('<td>')
      .addClass('mw-enhanced-rc')
      .append(
        (isnew ? flagNew : nbsp()),
        (isminor ? flagMinor : nbsp()),
        nbsp(), 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, timestampFormatted) {
    const revisionLink = $('<span>')
      .addClass('mw-enhanced-rc-time')
      .append(
        $('<a>')
          .text(timestampFormatted)
          .attr({
            href: permalink,
            title: pagename
          })
      )

    return revisionLink
  }

  static historyLinks (isnew, difflink, histlink, pagename) {
    // diff link
    const diffLink = $('<span>')
    const diffText = mw.msg('diff') // 'diff'
    if (isnew) {
      // new page, no link necessary
      diffLink.text(diffText)
    } else {
      diffLink.append($('<a>')
        .addClass(['mw-changeslist-diff', 'extiw'])
        .text(diffText)
        .attr('href', difflink)
      )
    }

    // hist link
    const histLink = $('<span>')
      .append(
        $('<a>')
          .addClass('mw-changeslist-history')
          .text(mw.msg('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')
      .text(mw.msg('cur')) // 'cur'
      .attr('href', curdifflink)

    // prev link
    const prevText = mw.msg('last') // 'prev'
    let prevLink
    if (isnew) {
      prevLink = prevText
    } else {
      prevLink = $('<a>')
        .addClass('mw-changeslist-diff')
        .text(prevText)
        .attr({
          href: difflink,
          title: pagename
        })
      prevLink = prevLink[0].outerHTML
    }

    const textInParen = mw.msg('parentheses').replace('$1',
      curLink[0].outerHTML +
      mw.msg('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 = mw.msg('nchanges', changesCount)
    if (isnew) {
      changesLinkWrapper.append(changesLinkText)
    } else {
      changesLinkWrapper.append(
        $('<a>')
          .addClass('mw-changeslist-groupdiff')
          .text(changesLinkText)
          .attr({
            href: difflink,
            title: pagename
          })
      )
    }

    // history link
    const historyLink = $('<a>')
      .addClass('mw-changeslist-history')
      .text(mw.msg('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, newsizeFormatted, diffsizeFormatted, noWrapper) {
    const characterDiff = $(isBigEdit ? '<strong>' : '<span>')
      .addClass('mw-diff-bytes')
      .text(diffsizeFormatted)
      .attr({
        dir: 'ltr',
        title: mw.msg('rc-change-size-new', newsizeFormatted)
      })

    if (diffsize === 0) {
      characterDiff.addClass('mw-plusminus-null')
    } else if (diffsize > 0) {
      characterDiff.addClass('mw-plusminus-pos')
    } else {
      characterDiff.addClass('mw-plusminus-neg')
    }

    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 +=
          ' ' +
          mw.msg('parentheses',
            mw.msg('ntimes', userinfo.userRevcount)
          )
      }

      groupUsersArray.push(groupUser)
    })

    const textInBrackets = mw.msg('brackets').replace('$1',
      groupUsersArray.join(mw.msg('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')
          .text(mw.msg('talkpagelinktext'))
          .attr({
            href: usertalklink,
            title: `${nsData.localNamespacesLocalized[3]}:${username}`
          })
      )

    // user contribs link
    const userContribsLinkWrapper = $('<span>')
      .append(
        $('<a>')
          .addClass('mw-usertoollinks-contribs')
          .text(mw.msg('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.timestampDateFormatted, revdata.timestampTimeFormatted
    )

    // ====== 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.newsizeFormatted, revdata.diffsizeFormatted
      )),

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

    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.timestampDateFormatted, revgroupdata.timestampTimeFormatted
    )

    // ====== 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.newsizeFormatted, revgroupdata.diffsizeFormatted
      ),

      // 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.timestampFormatted
      )),

      // cur-prev links
      (OutputMaker.curPrevLinks(
        revdata.curdifflink, revdata.isnew,
        revdata.difflink, revdata.pagename
      )),

      // character diff
      (OutputMaker.characterDiff(
        revdata.isBigEdit, revdata.diffsize,
        revdata.newsizeFormatted, revdata.diffsizeFormatted,
        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')

    return cell
  }
}

const nbsp = () => document.createTextNode(String.fromCodePoint(0x00A0))
const br = () => document.createElement('br')
Advertisement