ArchTranslator

Useful tools for ArchWiki translators, now written in TypeScript.

// ==UserScript==
// @name        ArchTranslator
// @namespace   bb89542e-b358-4be0-8c01-3797d1f3a1e3
// @match       https://wiki.archlinux.org/*
// @grant       none
// @version     2.0.3
// @author      bonk-dev
// @description Useful tools for ArchWiki translators, now written in TypeScript.
// @icon        https://gitlab.archlinux.org/uploads/-/system/group/avatar/23/iconfinder_archlinux_386451.png
// @license     MIT
// @run-at      document-start
// ==/UserScript==
"use strict";
(() => {
  // src/Utilities/MediaWikiJsApi.ts
  function isMwApiReady() {
    return typeof mw !== "undefined";
  }
  function getMwApi() {
    return mw;
  }

  // src/Injection/InjectionAgents.ts
  var DocumentLoadAgent = class {
    start() {
      return new Promise((resolve) => {
        window.addEventListener("load", () => {
          resolve();
        });
      });
    }
  };
  var StartupLoadAgent = class {
    constructor() {
      this._observer = null;
    }
    start() {
      if (isMwApiReady()) {
        return Promise.resolve();
      }
      return new Promise((resolve, reject) => {
        const checkScriptElement = (scriptElement) => {
          const srcUrl = new URL(scriptElement.src);
          const modules = srcUrl.searchParams.get("modules");
          if (modules === "startup") {
            scriptElement.addEventListener("load", () => {
              this._observer?.disconnect();
              resolve();
            });
            scriptElement.addEventListener("error", (e) => {
              reject(e);
            });
          }
        };
        const scripts = document.querySelectorAll("script[src]");
        if (scripts.length > 0) {
          for (const script of scripts) {
            checkScriptElement(script);
          }
        }
        this._observer = new MutationObserver((mutations) => {
          for (const mutation of mutations) {
            if (mutation.type !== "childList" || mutation.addedNodes.length <= 0 || mutation.target.nodeName !== "HEAD" || mutation.addedNodes[0].nodeName !== "SCRIPT") continue;
            const scriptElement = mutation.addedNodes[0];
            if (scriptElement.src.length <= 0) continue;
            checkScriptElement(scriptElement);
          }
        });
        this._observer.observe(document.documentElement, {
          childList: true,
          subtree: true
        });
      });
    }
  };

  // src/Injection/InjectionManager.ts
  var mediaWikiHookFunction = null;
  var onHookReady = null;
  var InjectionManager = class {
    constructor() {
      this._callbacks = {
        [0 /* DocumentLoad */]: {
          callbacks: [],
          fired: false
        },
        [1 /* MediaWikiStartup */]: {
          callbacks: [],
          fired: false
        },
        [2 /* JQueryEditForm */]: {
          callbacks: [],
          fired: false,
          jqueryElement: null
        }
      };
      this._agentsStarted = false;
    }
    startAgents() {
      onHookReady = () => {
        this.fire(1 /* MediaWikiStartup */);
        getMwApi().hook("wikipage.editform").add((jQueryEditForm) => {
          this.fireEditForm(jQueryEditForm);
        });
      };
      if (this._agentsStarted) {
        throw new Error("Agents were already started");
      }
      this._agentsStarted = true;
      const loadAgent = new DocumentLoadAgent();
      loadAgent.start().then(() => {
        this.fire(0 /* DocumentLoad */);
      });
      const startupAgent = new StartupLoadAgent();
      startupAgent.start().then(() => {
        if (getMwApi().hook == null) {
          console.debug("hook is null. Hijacking mw.hook");
          Object.defineProperty(getMwApi(), "hook", {
            get() {
              return mediaWikiHookFunction;
            },
            set(v) {
              mediaWikiHookFunction = v;
              console.debug("mw.hook hijacked");
              if (onHookReady != null) {
                onHookReady();
              }
            }
          });
        } else {
          if (onHookReady != null) {
            onHookReady();
          }
        }
      });
    }
    on(step, callback) {
      if (this._callbacks[step].fired) {
        callback();
      } else {
        this._callbacks[step].callbacks.push(callback);
      }
    }
    /**
     * Fired exclusively on edit pages. The manager will pass the JQuery editForm object as the first parameter.
     */
    onEditForm(callback) {
      const info = this._callbacks[2 /* JQueryEditForm */];
      if (info.fired) {
        callback(info.jqueryElement);
      } else {
        info.callbacks.push(callback);
      }
    }
    fire(step) {
      const info = this._callbacks[step];
      for (const callback of info.callbacks) {
        callback();
      }
      info.fired = true;
    }
    fireEditForm(jqueryEditForm) {
      const info = this._callbacks[2 /* JQueryEditForm */];
      info.jqueryElement = jqueryEditForm;
      for (const callback of info.callbacks) {
        callback(jqueryEditForm);
      }
      info.fired = true;
    }
  };

  // src/Tools/Utils/CollapsibleFooter.ts
  function makeCollapsibleFooter($content, $toggler, storeKey) {
    const collapsedVal = "0";
    const expandedVal = "1";
    const isCollapsed = mw.storage.get(storeKey) !== expandedVal;
    $toggler.addClass("mw-editfooter-toggler").prop("tabIndex", 0).attr("role", "button");
    $content.makeCollapsible({
      $customTogglers: $toggler,
      linksPassthru: true,
      plainMode: true,
      collapsed: isCollapsed
    });
    $toggler.addClass(isCollapsed ? "mw-icon-arrow-collapsed" : "mw-icon-arrow-expanded");
    $content.on("beforeExpand.mw-collapsible", () => {
      $toggler.removeClass("mw-icon-arrow-collapsed").addClass("mw-icon-arrow-expanded");
      mw.storage.set(storeKey, expandedVal);
    });
    $content.on("beforeCollapse.mw-collapsible", () => {
      $toggler.removeClass("mw-icon-arrow-expanded").addClass("mw-icon-arrow-collapsed");
      mw.storage.set(storeKey, collapsedVal);
    });
  }

  // src/Tools/Utils/ToolManager.ts
  var toolInProgressClass = "at-tool-inProgress";
  function sideTool(toolInfo) {
    const element = document.createElement("a");
    element.innerHTML = toolInfo.displayText;
    element.addEventListener("click", (e) => {
      if (element.parentElement.classList.contains(toolInProgressClass)) {
        return;
      }
      const result = toolInfo.handler(e);
      if (result instanceof Promise) {
        element.parentElement.classList.add(toolInProgressClass);
        result.finally(() => {
          element.parentElement.classList.remove(toolInProgressClass);
        });
      }
    });
    return {
      name: toolInfo.name,
      toolElement: element,
      showCallback: toolInfo.showCallback
    };
  }
  var ToolManager = class _ToolManager {
    constructor() {
      this._sidebarToolSectionAddedToDom = false;
      this._footerTools = [];
      this._footerToolContainer = null;
      if (_ToolManager._instance != null) {
        throw new Error("ToolManager was initialized already");
      }
      _ToolManager._instance = this;
      this._sidebarToolSection = _ToolManager._createToolSectionElement();
    }
    static {
      this._instance = null;
    }
    /**
     * Adds the ArchTranslator tool section to the DOM
     * @param parent If not null, adds the tool section to the specified element (otherwise looks for #vector-page-tools)
     */
    addSidebarToPage(parent = null) {
      if (this._sidebarToolSectionAddedToDom) {
        throw new Error("The tool section was added to DOM already");
      }
      if (parent == null) {
        parent = document.getElementById("vector-page-tools");
        if (parent == null) {
          throw new Error("Could not find vector-page-tools");
        }
      }
      parent.appendChild(this._sidebarToolSection.rootElement);
      this._sidebarToolSectionAddedToDom = true;
    }
    /**
     * Adds the footer tools to the DOM
     * @param editForm The JQuery element from editForm hook
     */
    addFooterToPage(editForm) {
      if (this._footerToolContainer != null) {
        throw new Error("Footer tool container was already initialized");
      }
      this._footerToolContainer = editForm;
      for (const tool of this._footerTools) {
        this._addFooterToolToContainer(tool);
      }
    }
    /**
     * Adds a custom tool to the sidebar
     * @param tool
     * @param currentPageInfo Used to hide useless tools based on the info about the current page
     */
    addSidebarTool(tool, currentPageInfo) {
      if (tool.showCallback != null && !tool.showCallback(currentPageInfo)) {
        return;
      }
      const listItem = document.createElement("li");
      listItem.id = `t-at-${tool.name}`;
      listItem.classList.add("mw-list-item");
      listItem.appendChild(tool.toolElement);
      this._sidebarToolSection.list.appendChild(listItem);
    }
    /**
     * Adds a custom tool to the footer as a collapsible element
     * @param tool
     */
    addFooterTool(tool) {
      const registeredTool = this._registerFooterTool(tool);
      this._addFooterToolToContainer(registeredTool);
      makeCollapsibleFooter(
        $(registeredTool.tool),
        $(registeredTool.toggler),
        tool.storeKey
      );
    }
    _addFooterToolToContainer(tool) {
      if (this._footerToolContainer != null) {
        this._footerToolContainer.find(".templatesUsed").before(tool.container);
      }
    }
    _registerFooterTool(tool) {
      const parentElement = document.createElement("div");
      parentElement.className = tool.name;
      const togglerElement = document.createElement("div");
      togglerElement.className = `${tool.name}-toggler`;
      togglerElement.innerHTML = `<p>${tool.header}</p>`;
      parentElement.appendChild(togglerElement);
      parentElement.appendChild(tool.toolElement);
      if (tool.toolElement.tagName === "UL" || tool.toolElement.tagName === "OL" || tool.toolElement.tagName === "DL") {
        tool.toolElement.classList.add("mw-editfooter-list");
      }
      const registeredTool = {
        container: parentElement,
        tool: tool.toolElement,
        toggler: togglerElement
      };
      this._footerTools.push(registeredTool);
      return registeredTool;
    }
    /** Returns the singleton instance of ToolManager */
    static get instance() {
      if (this._instance == null) {
        return new _ToolManager();
      }
      return this._instance;
    }
    static _createToolSectionElement() {
      const toolContainer = document.createElement("div");
      toolContainer.id = "p-arch-translator";
      toolContainer.classList.add("vector-menu", "mw-portlet");
      const menuHeading = document.createElement("div");
      menuHeading.classList.add("vector-menu-heading");
      menuHeading.innerText = "Arch Translator";
      toolContainer.appendChild(menuHeading);
      const menuContent = document.createElement("div");
      menuContent.classList.add("vector-menu-content");
      toolContainer.appendChild(menuContent);
      const menuList = document.createElement("ul");
      menuList.classList.add("vector-menu-content-list");
      menuContent.appendChild(menuList);
      return {
        rootElement: toolContainer,
        list: menuList
      };
    }
  };

  // src/Internalization/I18nConstants.ts
  var LanguageInfo = class {
    constructor(subtag, englishName, localizedName, customKey) {
      this.subtag = subtag;
      this.englishName = englishName;
      this.localizedName = localizedName;
      this.key = customKey ?? this.englishName;
    }
  };
  var LanguagesInfo = {
    Arabic: new LanguageInfo("ar", "Arabic", "\u0627\u0644\u0639\u0631\u0628\u064A\u0629"),
    Bangla: new LanguageInfo(null, "Bangla", "\u09AC\u09BE\u0982\u09B2\u09BE"),
    Bosnian: new LanguageInfo("bs", "Bosnian", "Bosanski"),
    Bulgarian: new LanguageInfo("bg", "Bulgarian", "\u0411\u044A\u043B\u0433\u0430\u0440\u0441\u043A\u0438"),
    Cantonese: new LanguageInfo(null, "Cantonese", "\u7CB5\u8A9E"),
    Catalan: new LanguageInfo("ca", "Catalan", "Catal\xE0"),
    ChineseClassical: new LanguageInfo(null, "Chinese (Classical)", "\u6587\u8A00\u6587", "ChineseClassical"),
    ChineseSimplified: new LanguageInfo("zh-hans", "Chinese (Simplified)", "\u7B80\u4F53\u4E2D\u6587", "ChineseSimplified"),
    ChineseTraditional: new LanguageInfo("zh-hant", "Chinese (Traditional)", "\u6B63\u9AD4\u4E2D\u6587", "ChineseTraditional"),
    Croatian: new LanguageInfo("hr", "Croatian", "Hrvatski"),
    Czech: new LanguageInfo("cs", "Czech", "\u010Ce\u0161tina"),
    Danish: new LanguageInfo("da", "Danish", "Dansk"),
    Dutch: new LanguageInfo("nl", "Dutch", "Nederlands"),
    English: new LanguageInfo("en", "English", "English"),
    Esperanto: new LanguageInfo(null, "Esperanto", "Esperanto"),
    Finnish: new LanguageInfo("fi", "Finnish", "Suomi"),
    French: new LanguageInfo("fr", "French", "Fran\xE7ais"),
    German: new LanguageInfo("de", "German", "Deutsch"),
    Greek: new LanguageInfo("el", "Greek", "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC"),
    Hebrew: new LanguageInfo("he", "Heberew", "\u05E2\u05D1\u05E8\u05D9\u05EA"),
    Hungarian: new LanguageInfo("hu", "Hungarian", "Magyar"),
    Indonesian: new LanguageInfo("id", "Indonesian", "Bahasa Indonesia"),
    Italian: new LanguageInfo("it", "Italian", "Italiano"),
    Japanese: new LanguageInfo("ja", "Japanese", "\u65E5\u672C\u8A9E"),
    Korean: new LanguageInfo("ko", "Korean", "\uD55C\uAD6D\uC5B4"),
    Lithuanian: new LanguageInfo("lt", "Lithuanian", "Lietuvi\u0173"),
    NorwegianBokmal: new LanguageInfo(null, "Norwegian (Bokm\xE5l)", "Norsk Bokm\xE5l", "NorwegianBokmal"),
    Polish: new LanguageInfo("pl", "Polish", "Polski"),
    Portuguese: new LanguageInfo("pt", "Portuguese", "Portugu\xEAs"),
    Romanian: new LanguageInfo(null, "Romanian", "Rom\xE2n\u0103"),
    Russian: new LanguageInfo("ru", "Russian", "\u0420\u0443\u0441\u0441\u043A\u0438\u0439"),
    Serbian: new LanguageInfo("sr", "Serbian", "\u0421\u0440\u043F\u0441\u043A\u0438 (Srpski)"),
    Slovak: new LanguageInfo("sk", "Slovak", "Sloven\u010Dina"),
    Spanish: new LanguageInfo("es", "Spanish", "Espa\xF1ol"),
    Swedish: new LanguageInfo("sv", "Swedish", "Svenska"),
    Thai: new LanguageInfo("th", "Thai", "\u0E44\u0E17\u0E22"),
    Turkish: new LanguageInfo("tr", "Turkish", "T\xFCrk\xE7e"),
    Ukrainian: new LanguageInfo("uk", "Ukrainian", "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430"),
    Vietnamese: new LanguageInfo(null, "Vietnamese", "Ti\u1EBFng Vi\u1EC7t"),
    Quechua: new LanguageInfo(null, "Quechua", "Runa simi")
  };
  var validLangPostfixes = Object.values(LanguagesInfo).map((i) => "(" + i.localizedName + ")");
  var validLangSubtags = Object.values(LanguagesInfo).filter((i) => i.subtag != null).map((i) => i.subtag);
  function isTranslated(title) {
    for (const postfix of validLangPostfixes) {
      if (title.endsWith(postfix)) {
        return true;
      }
    }
    return false;
  }
  function removeLanguagePostfix(pageOrTitle) {
    const remove = (target, postfix) => {
      return target.substring(0, target.length - 1 - postfix.length);
    };
    for (const postfix of validLangPostfixes) {
      if (!pageOrTitle.endsWith(postfix)) {
        continue;
      }
      const split = pageOrTitle.split("/");
      if (split.length <= 1) {
        return remove(pageOrTitle, postfix);
      } else {
        return split.map((part) => remove(part, postfix)).join("/");
      }
    }
    return pageOrTitle;
  }
  function getLangInfoFor(key) {
    const info = LanguagesInfo[key];
    if (info == null) {
      throw new Error(`Invalid language key: ${key}`);
    }
    return info;
  }

  // src/Utilities/Api/MediaWikiApiClient.ts
  var MAX_PAGE_QUERY_LIMIT = 50;
  var getPageContent = async (pageName) => {
    pageName = titleToPageName(pageName);
    const response = await fetch(
      `/api.php?action=query&prop=revisions&titles=${pageName}&rvslots=*&rvprop=ids|content&format=json&formatversion=2`
    );
    const jsonObj = await response.json();
    const revision = jsonObj.query.pages[0].revisions[0];
    return {
      revisionId: revision.revid,
      content: revision.slots.main.content,
      title: jsonObj.query.pages[0].title
    };
  };
  var getPageInfos = async (titles) => {
    if (titles.length <= 0) {
      throw new Error("Titles array must be larger than 0");
    }
    const fetchInfo = async (titles2) => {
      const encodedTitles = encodeURIComponent(titles2.join("|"));
      const apiResponse = await fetch(`/api.php?action=query&prop=info&titles=${encodedTitles}&format=json`);
      if (!apiResponse.ok) {
        throw new Error("API request failed: " + apiResponse.statusText);
      }
      return await apiResponse.json();
    };
    const groupLimit = (bigArray, limit) => {
      if (limit <= 0) {
        throw new Error("groupLimit 'limit' parameter must be larger than 0");
      }
      if (bigArray.length <= limit) {
        return [bigArray];
      }
      const parentArray = [];
      let addedTitles = 0;
      while (addedTitles < bigArray.length) {
        const childArray = [];
        const childSize = Math.min(bigArray.length - addedTitles, limit);
        const startingIndex = addedTitles;
        for (let i = 0; i < childSize; ++i) {
          childArray.push(bigArray[i + startingIndex]);
          addedTitles++;
        }
        parentArray.push(childArray);
      }
      return parentArray;
    };
    const groups = groupLimit(titles, MAX_PAGE_QUERY_LIMIT);
    const responses = [];
    for (const titleGroup of groups) {
      responses.push(await fetchInfo(titleGroup));
    }
    return Object.assign({}, ...responses);
  };
  var getPageLinks = async (titles) => {
    const titleConcat = encodeURIComponent(titles.join("|"));
    const response = await fetch(`/api.php?action=query&prop=links&titles=${titleConcat}&format=json`);
    if (!response.ok) {
      throw new Error("API request failed: " + response.statusText);
    }
    return await response.json();
  };

  // src/Storage/IndexedDbPromiseWrappers.ts
  var openDb = (name, version) => {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name, version);
      request.onsuccess = (e) => {
        resolve({
          type: 0 /* Success */,
          // @ts-ignore
          database: e.target.result
        });
      };
      request.onblocked = (e) => {
        resolve({
          type: 1 /* Blocked */
        });
      };
      request.onupgradeneeded = (e) => {
        resolve({
          type: 2 /* UpgradeNeeded */,
          // @ts-ignore
          database: e.target.result,
          oldVer: e.oldVersion,
          newVer: e.newVersion
        });
      };
      request.onerror = (e) => {
        reject(e);
      };
    });
  };
  var wrapGenericRequest = (request) => {
    return new Promise((resolve, reject) => {
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve(request.result);
      };
    });
  };
  var get = async (store, key) => {
    const request = store.get(key);
    return await wrapGenericRequest(request);
  };
  var put = async (store, value, key) => {
    const request = store.put(value, key);
    return await wrapGenericRequest(request);
  };

  // src/Storage/ScriptDb.ts
  var ARCH_TRANSLATOR_DB_NAME = "ArchTranslator";
  var ARCH_TRANSLATOR_DB_VERSION = 1;
  var CACHED_PAGES_STORE = "cached_pages";
  var SETTINGS_STORE = "settings";
  var SETTINGS_LANG_KEY = "language";
  var DEFAULT_SETTINGS = {
    [SETTINGS_LANG_KEY]: "Polish"
  };
  var database = null;
  var getDb = () => {
    if (database == null) {
      throw new Error("Non-null database instance was requested, but the instance was null");
    }
    return database;
  };
  var upgradeDb = async (oldVersion, newVersion) => {
    console.debug(`upgradeDb: Upgrading from ${oldVersion} to ${newVersion}`);
    getDb().createObjectStore(CACHED_PAGES_STORE, {
      keyPath: "pageName"
    });
    getDb().createObjectStore(SETTINGS_STORE);
  };
  var setupDb = async () => {
    console.debug(`setupDb: ${ARCH_TRANSLATOR_DB_NAME} (${ARCH_TRANSLATOR_DB_VERSION})`);
    try {
      const dbResult = await openDb(ARCH_TRANSLATOR_DB_NAME, ARCH_TRANSLATOR_DB_VERSION);
      switch (dbResult.type) {
        case 0 /* Success */:
          database = dbResult.database;
          return true;
        case 2 /* UpgradeNeeded */:
          database = dbResult.database;
          await upgradeDb(dbResult.oldVer, dbResult.newVer);
          return true;
        default:
          console.error(dbResult);
          return false;
      }
    } catch (e) {
      console.error(e);
      return false;
    }
  };
  var getCachedPageInfo = async (pageName) => {
    const db = getDb();
    const tr = db.transaction([CACHED_PAGES_STORE]);
    const store = tr.objectStore(CACHED_PAGES_STORE);
    return await get(store, pageName);
  };
  var setCachedPageInfo = async (pageInfo) => {
    const db = getDb();
    const tr = db.transaction(CACHED_PAGES_STORE, "readwrite");
    const store = tr.objectStore(CACHED_PAGES_STORE);
    await put(store, pageInfo);
  };
  var getDefaultSettingValue = (key) => {
    return DEFAULT_SETTINGS[key];
  };
  var getCurrentLanguage = async () => {
    const db = getDb();
    const tr = db.transaction(SETTINGS_STORE);
    const store = tr.objectStore(SETTINGS_STORE);
    let languageKey = await get(store, SETTINGS_LANG_KEY);
    if (languageKey == null) {
      languageKey = getDefaultSettingValue(SETTINGS_LANG_KEY);
    }
    return getLangInfoFor(languageKey);
  };
  var setCurrentLanguage = async (info) => {
    const db = getDb();
    const tr = db.transaction(SETTINGS_STORE, "readwrite");
    const store = tr.objectStore(SETTINGS_STORE);
    await put(store, info.key, SETTINGS_LANG_KEY);
  };

  // src/Utilities/PageUtils.ts
  var cachedPageContent = null;
  function getCurrentPageType() {
    const isArticle = getMwApi().config.values.wgIsArticle;
    const action = getMwApi().config.values.wgAction;
    switch (action) {
      case "edit":
        const isEditable = getMwApi().config.values.wgIsProbablyEditable;
        const editMessage = getMwApi().config.values.wgEditMessage;
        if (editMessage === "creating" /* Creating */) {
          return 3 /* CreateEditor */;
        }
        return isEditable ? 2 /* Editor */ : 4 /* ViewOnlyEditor */;
      case "view":
        const revisionId = getMwApi().config.values.wgCurRevisionId;
        if (revisionId === 0) {
          return 1 /* ReadNonExisting */;
        }
        return isArticle ? 0 /* Read */ : 5 /* Other */;
      default:
        return 5 /* Other */;
    }
  }
  function getCurrentPageInfo() {
    const title = getMwApi().config.values.wgTitle;
    const pageName = getMwApi().config.values.wgPageName;
    const isRedirect = getMwApi().config.values.wgIsRedirect;
    const revisionId = getMwApi().config.values.wgCurRevisionId;
    return {
      isRedirect,
      isTranslated: isTranslated(title),
      latestRevisionId: revisionId,
      pageName,
      pageType: getCurrentPageType(),
      title
    };
  }
  function cacheCurrentPageContent(content) {
    cachedPageContent = content;
  }
  async function getCurrentPageContent() {
    if (cachedPageContent != null) {
      console.debug(`getCurrentPageContent: cache hit`);
      return cachedPageContent;
    }
    const pageName = getMwApi().config.values.wgPageName;
    const type = getCurrentPageType();
    switch (type) {
      case 2 /* Editor */:
      case 3 /* CreateEditor */:
      case 4 /* ViewOnlyEditor */:
        console.warn("cachedPageContent was null on edit page. You might be requesting the page content too soon.");
        return (await getPageContent(pageName)).content;
      case 0 /* Read */:
        return (await getPageContent(pageName)).content;
      case 1 /* ReadNonExisting */:
        return "";
      case 5 /* Other */:
        throw new Error("Cannot return content for page of 'Other' type");
    }
  }
  async function getEnglishRevisionId() {
    const info = getCurrentPageInfo();
    if (!info.isTranslated) {
      throw new Error("The current page is not a translation");
    }
    const englishPageName = removeLanguagePostfix(info.pageName);
    if (englishPageName === info.pageName) {
      console.warn(`getEnglishRevisionId: Could not get the English for the ${info.pageName}`);
      return null;
    }
    const cachedInfo = await getCachedPageInfo(englishPageName);
    console.debug(`getEnglishRevisionId english name: ${englishPageName}`);
    if (cachedInfo != null) {
      console.debug("getEnglishRevisionId: cache hit");
      return cachedInfo.latestRevisionId;
    }
    return null;
  }
  function pageNameToTitle(pageName) {
    return pageName.replaceAll("_", "");
  }
  function titleToPageName(title) {
    return title.replaceAll(" ", "_");
  }

  // src/Tools/SidebarTools.ts
  var copyCurrentRevisionIdTool = () => {
    const showCallback = (info) => {
      return info.pageType !== 3 /* CreateEditor */ && info.pageType !== 1 /* ReadNonExisting */;
    };
    const toolHandler = async () => {
      const revisionId = getMwApi().config.values.wgCurRevisionId;
      await window.navigator.clipboard.writeText(revisionId.toString());
    };
    return sideTool({
      name: "copy-current-revision-id",
      displayText: "Copy revision ID",
      handler: toolHandler,
      showCallback
    });
  };
  var copyEnglishRevisionIdTool = () => {
    const handler = async () => {
      const englishRevisionId = await getEnglishRevisionId();
      console.debug("English revision id: " + englishRevisionId);
      await window.navigator.clipboard.writeText(englishRevisionId?.toString() ?? "null");
    };
    const showCallback = (info) => {
      if (!info.isTranslated) {
        console.debug("Hiding copy English revision ID tool because current page is not a translation");
        return false;
      }
      return true;
    };
    return sideTool({
      name: "copy-english-revision-id",
      displayText: "Copy latest English revision ID",
      handler,
      showCallback
    });
  };
  var createTranslationTool = () => {
    const handler = async () => {
      const pageInfo = getCurrentPageInfo();
      const currentLang = await getCurrentLanguage();
      const translationPageName = `${pageInfo.pageName}_(${currentLang.localizedName})`;
      const target = `/index.php?title=${encodeURIComponent(translationPageName)}&action=edit`;
      console.debug(`createTranslationTool: navigating to ${target}`);
      window.location.assign(target);
    };
    const showCallback = (info) => {
      if (info.isTranslated) {
        console.debug("Hiding create translation tool because current page is already a translation");
        return false;
      }
      return true;
    };
    return sideTool({
      name: "create-translation",
      displayText: "Translate this page",
      handler,
      showCallback
    });
  };
  var changeActiveLanguageTool = () => {
    let anchorElement = null;
    let select = null;
    const saveLanguage = async () => {
      if (select == null) {
        throw new Error("Tried to save the language when the UI was not yet created");
      }
      const newLanguage = getLangInfoFor(select.value);
      await setCurrentLanguage(newLanguage);
      alert(`The Arch Translator active language was changed to ${newLanguage.localizedName}. Please refresh the page.`);
      select.parentElement.classList.add("hidden");
    };
    const createChangeUi = async () => {
      const parent = document.createElement("div");
      parent.classList.add("hidden");
      const selectElement = document.createElement("select");
      for (const [langKey, langInfo] of Object.entries(LanguagesInfo)) {
        const option = document.createElement("option");
        option.value = langKey;
        option.innerText = langInfo.localizedName;
        selectElement.appendChild(option);
      }
      selectElement.onchange = saveLanguage;
      const currentLang = await getCurrentLanguage();
      selectElement.value = currentLang.key;
      select = selectElement;
      parent.appendChild(selectElement);
      return parent;
    };
    const handler = async () => {
      if (anchorElement == null) {
        anchorElement = document.getElementById("t-at-change-language");
      }
      if (select == null) {
        const changeUi = await createChangeUi();
        anchorElement.after(changeUi);
      }
      select.parentElement.classList.toggle("hidden");
    };
    return sideTool({
      name: "change-language",
      displayText: "Change active language",
      handler
    });
  };
  var allSidebarTools = [
    changeActiveLanguageTool(),
    copyCurrentRevisionIdTool(),
    copyEnglishRevisionIdTool(),
    createTranslationTool()
  ];

  // src/Utilities/WikiTextParser.ts
  var WikiLink = class {
    constructor(rawLink) {
      if (rawLink.startsWith("[[")) {
        rawLink = rawLink.substring(2);
      }
      if (rawLink.endsWith("]]")) {
        rawLink = rawLink.substring(0, rawLink.length - 2);
      }
      const split = rawLink.split("|");
      this._link = split[0];
      const headerSplit = this._link.split("#");
      if (headerSplit.length > 1) {
        this._link = headerSplit[0];
        this._linkedHeader = headerSplit[1];
      } else {
        this._linkedHeader = null;
      }
      this._alias = split.length > 1 ? split[1] : null;
      if (split[0].startsWith(":")) {
        this._linkType = "category" /* Category */;
      } else if (split[0].startsWith("#")) {
        this._linkType = "header" /* Header */;
      } else {
        this._linkType = "article" /* Article */;
      }
    }
    get link() {
      return this._link;
    }
    get alias() {
      return this._alias;
    }
    get linkType() {
      return this._linkType;
    }
    get header() {
      return this._linkedHeader;
    }
    get linkWithHeader() {
      const header = this.header != null ? `#${this.header}` : "";
      return this.link + header;
    }
  };
  var WikiTextParser = class {
    constructor() {
      this._redirects = [];
      this._magicWords = [];
      this._categories = [];
      this._interlanguageLinks = {};
      this._templates = [];
      this._relatedArticleElements = [];
      this._parsingContent = false;
      this._parseMagicWords = true;
      this._rawContentLines = [];
      this._links = [];
    }
    get headerText() {
      let sortedLinks = [];
      for (let langSubtag of Object.keys(this._interlanguageLinks).sort()) {
        sortedLinks.push(this._interlanguageLinks[langSubtag]);
      }
      const finalArray = [
        ...this._redirects,
        ...this._magicWords,
        ...this._categories,
        ...sortedLinks,
        ...this._templates,
        ...this._relatedArticleElements
      ];
      return finalArray.join("\n") + "\n";
    }
    get pageBodyText() {
      return this._rawContentLines.join("\n");
    }
    get pageContent() {
      return this.headerText + this.pageBodyText;
    }
    parse(wikiTextContent) {
      const lines = wikiTextContent.split("\n");
      for (let line of lines) {
        this.parseLine(line);
      }
    }
    parseLine(line) {
      if (!this._parsingContent) {
        const headerResult = this._parseHeaderLine(line);
        if (headerResult === 6 /* EndOfHeader */ || headerResult === 0 /* Redirect */) {
          this._parsingContent = true;
        }
        return headerResult;
      } else {
        return this._parseContentLine(line);
      }
    }
    get localizableLinks() {
      const firstCapitalLetter = (str) => {
        if (str.length < 2) {
          return str.toUpperCase();
        }
        return `${str[0].toUpperCase()}${str.substring(1)}`;
      };
      return [
        ...new Set(this._links.filter((l) => !isTranslated(l.link) && (l.linkType === "category" /* Category */ || l.linkType === "article" /* Article */)).map((l) => firstCapitalLetter(l.link)))
      ];
    }
    _parseHeaderLine(line) {
      const isBlank = (str) => {
        return !str || /^\s*$/.test(str);
      };
      if (isBlank(line)) {
        return 7 /* EmptyLine */;
      }
      if (line.startsWith("#REDIRECT")) {
        this._redirects.push(line);
        this._log("Found redirect: " + line);
        return 0 /* Redirect */;
      }
      for (let subtag of validLangSubtags) {
        if (line.startsWith(`[[${subtag}:`)) {
          this.addInterlanguageLink(line, subtag);
          this._parseMagicWords = false;
          return 3 /* InterlanguageLink */;
        }
      }
      if (line.startsWith("[[Category")) {
        this._addCategory(line);
        this._parseMagicWords = false;
        return 2 /* Category */;
      } else if (line.startsWith("{{Related")) {
        this._addRelatedArticleElement(line);
        return 5 /* RelatedArticle */;
      } else if (line.startsWith("{{") || line.startsWith("__")) {
        if (this._parseMagicWords) {
          this._addMagicWord(line);
          return 1 /* MagicWord */;
        } else {
          this.addTemplate(line);
          return 4 /* Template */;
        }
      } else {
        this._rawContentLines.push(line);
        return 6 /* EndOfHeader */;
      }
    }
    addInterlanguageLink(line, subtag) {
      this._interlanguageLinks[subtag] = line;
    }
    _addCategory(line) {
      this._categories.push(line);
    }
    _addMagicWord(line) {
      this._magicWords.push(line);
    }
    addTemplate(line) {
      this._templates.push(line);
    }
    _addRelatedArticleElement(line) {
      this._relatedArticleElements.push(line);
    }
    _parseContentLine(line) {
      this._rawContentLines.push(line);
      for (let match of line.matchAll(/\[\[(?!Wikipedia)(?!w)(?!mw)([^\[\]]*)]]/ig)) {
        const link = new WikiLink(match[1]);
        this._links.push(link);
      }
      return 8 /* ContentLine */;
    }
    _log(msg) {
      console.debug(`WikiTextParser: ${msg}`);
    }
  };
  var findRedirect = (wikiText) => {
    if (!wikiText.startsWith("#REDIRECT")) {
      return null;
    }
    const linkText = wikiText.substring("#REDIRECT ".length).trim();
    return new WikiLink(linkText);
  };

  // src/Tools/CurrentPageDumper.ts
  var cacheCurrentPage = async () => {
    const info = getCurrentPageInfo();
    if (info.pageType !== 0 /* Read */) return;
    if (info.isRedirect) {
      const content = await getCurrentPageContent();
      const redirectTarget = findRedirect(content);
      if (redirectTarget == null) {
        throw new Error("The current page is a redirect but no redirect link was found");
      }
      await setCachedPageInfo({
        latestRevisionId: info.latestRevisionId,
        pageName: info.pageName,
        redirectsTo: redirectTarget.linkWithHeader,
        type: "redirect" /* Redirect */
      });
    } else {
      await setCachedPageInfo({
        latestRevisionId: info.latestRevisionId,
        pageName: info.pageName,
        type: info.isTranslated ? "translated" /* Translated */ : "english" /* English */
      });
    }
  };

  // src/Utilities/TemplateUtils.ts
  var translateTemplate = (template, language) => {
    return language == null ? template : `${template} (${language.localizedName})`;
  };
  var buildTranslationStatusTemplate = (englishName, date, englishRevisionId, language) => {
    const templateName = translateTemplate("TranslationStatus", language);
    const formattedDate = date.toISOString().split("T")[0];
    return `{{${templateName}|${englishName}|${formattedDate}|${englishRevisionId}}}`;
  };

  // src/Tools/Workers/NewArticleWorker.ts
  var NewArticleWorker = class {
    constructor(pageInfo, language, englishRevisionId) {
      this._info = pageInfo;
      this._language = language;
      this._englishRevisionId = englishRevisionId ?? 0;
    }
    willRun() {
      return this._info.pageType === 3 /* CreateEditor */ && this._info.isTranslated;
    }
    run(parser) {
      if (!this.willRun()) return;
      console.debug("NewArticleWorker: running");
      const englishName = removeLanguagePostfix(this._info.pageName);
      console.debug(`NewArticleWorker: fetching content of ${englishName}`);
      const englishTitle = pageNameToTitle(englishName);
      parser.addInterlanguageLink(`[[en:${englishTitle}]]`, "en");
      const translationStatus = buildTranslationStatusTemplate(
        englishName,
        /* @__PURE__ */ new Date(),
        this._englishRevisionId,
        this._language
      );
      parser.addTemplate(translationStatus);
    }
  };

  // src/Tools/Workers/TranslatedArticlesWorker.ts
  var localizeLink = (link, lang) => {
    const subPageSplit = link.split("/");
    const prefix = ` (${lang.localizedName})`;
    if (subPageSplit.length < 2) {
      return link + prefix;
    }
    return subPageSplit.map((s) => s + prefix).join("/");
  };
  var TranslatedArticlesWorker = class {
    constructor(pageInfo) {
      this._info = pageInfo;
    }
    willRun() {
      return (this._info.pageType === 3 /* CreateEditor */ || this._info.pageType === 2 /* Editor */) && this._info.isTranslated;
    }
    async run(parser) {
      if (!this.willRun()) {
        return {
          existing: [],
          notExisting: [],
          redirects: []
        };
      }
      const language = await getCurrentLanguage();
      const linksWithPostfixes = parser.localizableLinks.map((l) => localizeLink(l, language));
      const info = await this._getPageInfosFor(linksWithPostfixes);
      console.debug(info.filter((i) => i.exists));
      const possibleRedirects = info.filter((r) => !r.exists).map((r) => removeLanguagePostfix(r.link));
      const redirectsAndOther = await this._findRedirects(possibleRedirects);
      console.debug(redirectsAndOther.filter((r) => r.redirects));
      const actualRedirects = redirectsAndOther.filter((r) => r.redirects && !isTranslated(r.redirectsTo)).map((r) => localizeLink(r.redirectsTo, language));
      const redirectResults = actualRedirects.length > 0 ? await this._getPageInfosFor(actualRedirects) : [];
      console.debug(redirectResults);
      const findRedirectSource = (localizedLink) => {
        const englishLink = removeLanguagePostfix(localizedLink);
        if (englishLink === localizedLink) return localizedLink;
        const original = redirectsAndOther.find((r) => r.redirectsTo === englishLink);
        return original.link;
      };
      return {
        existing: info.filter((i) => i.exists).map((r) => r.link),
        redirects: redirectResults.map((r) => {
          return {
            link: findRedirectSource(r.link),
            redirectsTo: removeLanguagePostfix(r.link),
            localizedRedirectTarget: r.link,
            exists: r.exists
          };
        }),
        notExisting: redirectsAndOther.filter((r) => !r.redirects).map((r) => localizeLink(r.link, language))
      };
    }
    async _getPageInfosFor(links) {
      const apiData = await getPageInfos(links);
      if (Array.isArray(typeof apiData.query.pages)) {
        return apiData.query.pages.map((p) => {
          return {
            link: p.title,
            exists: "missing" in p
          };
        });
      }
      const data = [];
      const keyedApiData = apiData;
      if ("pages" in keyedApiData.query) {
        const pageValues = Object.values(keyedApiData.query.pages);
        data.push(...pageValues.map((pageInfo) => {
          return {
            link: pageInfo.title,
            exists: !("missing" in pageInfo)
          };
        }));
      }
      if ("interwiki" in keyedApiData.query) {
        const interwikiValues = Object.values(keyedApiData.query.interwiki);
        data.push(...interwikiValues.map((iwInfo) => {
          return {
            link: iwInfo.title,
            exists: false
            // Just assume it does not exist TODO: for now
          };
        }));
      }
      return data;
    }
    async _findRedirects(links) {
      const apiData = await getPageInfos(links);
      const redirects = [];
      if (Array.isArray(typeof apiData.query.pages)) {
        apiData.query.pages.forEach((p) => {
          if ("redirect" in p) {
            redirects.push(p.title);
          }
        });
      } else {
        const keyedApiData = apiData;
        Object.values(keyedApiData.query.pages).forEach((p) => {
          if ("redirect" in p) {
            redirects.push(p.title);
          }
        });
      }
      if (redirects.length <= 0) {
        return [];
      }
      const linksApiData = await getPageLinks(redirects);
      const redirectsInfo = Object.values(linksApiData.query.pages).map((l) => {
        return {
          link: l.title,
          redirects: true,
          redirectsTo: "links" in l ? l.links[0].title : null
        };
      });
      for (const info of redirectsInfo) {
        if (info.redirectsTo == null) {
          console.warn(`API returned no links for the redirect "${info.link}". Fixing...`);
          const content = await getPageContent(titleToPageName(info.link));
          console.debug(content);
          const redirectLink = findRedirect(content.content);
          if (redirectLink == null) {
            throw new Error(`Could not get redirect target for link ${info.link}`);
          }
          info.redirectsTo = redirectLink.link;
        }
      }
      const nonRedirectsInfo = links.filter((l) => !redirects.includes(l)).map((l) => {
        return {
          link: l,
          redirects: false
        };
      });
      return [...redirectsInfo, ...nonRedirectsInfo];
    }
  };

  // src/Tools/TranslatedArticlesUi.ts
  var LOCALIZED_LINKS_UI_STORE_KEY = "mwedit-state-arch-translator-translated-art";
  var editFormLocalizedArticlesTable = null;
  var tableBody = null;
  var addTranslatedArticlesUi = ($editForm) => {
    const localizedArticlesUiHtml = `<div class="localizedArticlesUi">
            <div class="localizedArticlesUi-toggler">
                <p>AT - Localized articles:</p>
            </div>
            <table class="wikitable mw-editfooter-list">
                <thead>
                    <tr>
                        <th>Localized name</th>
                        <th>Redirected from</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td colspan="2">Please wait</td>
                    </tr>
                </tbody>
            </table>
        </div>`;
    $editForm.find(".templatesUsed").before(localizedArticlesUiHtml);
    const linksTable = $editForm.find(".localizedArticlesUi table");
    editFormLocalizedArticlesTable = linksTable[0];
    tableBody = $editForm.find(".localizedArticlesUi table tbody")[0];
    makeCollapsibleFooter(
      linksTable,
      $editForm.find(".localizedArticlesUi-toggler"),
      LOCALIZED_LINKS_UI_STORE_KEY
    );
  };
  var addWorkerResultToUi = (result) => {
    const compareRedirects = (a, b) => {
      const aOrder = a.exists ? 0 : 1;
      const bOrder = b.exists ? 0 : 1;
      return aOrder - bOrder;
    };
    const linkify = (title, followRedirects = true) => {
      return followRedirects ? `/title/${titleToPageName(title)}` : `/index.php?title=${encodeURIComponent(titleToPageName(title))}&redirect=no`;
    };
    if (editFormLocalizedArticlesTable == null || tableBody == null) {
      throw new Error("editFormLocalizedArticlesTable was not created yet");
    }
    if (result.existing.length <= 0 && result.redirects.length <= 0 && result.notExisting.length <= 0) {
      tableBody.innerHTML = '<td class="muted-link" colspan="2">No links were found in the page content</td>';
      return;
    }
    tableBody.innerHTML = "";
    const existingRedirectCell = `<td class="muted-link" rowspan="${result.existing.length}">N/A</td>`;
    let existingRedirectCellAdded = false;
    for (const existing of result.existing) {
      const row = document.createElement("tr");
      const className = "green-link";
      row.classList.add(className);
      row.innerHTML = `<td class="${className}"><a href="${linkify(existing)}">${existing}</td></a>`;
      if (!existingRedirectCellAdded) {
        row.innerHTML += existingRedirectCell;
        existingRedirectCellAdded = true;
      }
      tableBody.appendChild(row);
    }
    for (const redirect of result.redirects.sort(compareRedirects)) {
      const row = document.createElement("tr");
      const className = "blue-link";
      row.classList.add(className);
      const nameCell = document.createElement("td");
      nameCell.className = redirect.exists ? "green-link" : "red-link";
      nameCell.innerHTML = redirect.exists ? `<a href="${linkify(redirect.localizedRedirectTarget)}">${redirect.localizedRedirectTarget}</a>` : redirect.localizedRedirectTarget;
      row.appendChild(nameCell);
      const redirectCell = document.createElement("td");
      redirectCell.className = className;
      const redirectSrc = `<a href="${linkify(redirect.link, false)}">${redirect.link}</a>`;
      const redirectTarget = `<a href="${linkify(redirect.redirectsTo)}">${redirect.redirectsTo}</a>`;
      redirectCell.innerHTML = `${redirectSrc} -&gt; ${redirectTarget}`;
      row.appendChild(redirectCell);
      tableBody.appendChild(row);
    }
    const notExistingRedirectCell = `<td class="muted-link" rowspan="${result.notExisting.length}">N/A</td>`;
    let notExistingRedirectCellAdded = false;
    for (const notExisting of result.notExisting) {
      const row = document.createElement("tr");
      const className = "red-link";
      row.classList.add(className);
      row.innerHTML = `<td class="${className}">${notExisting}</td>`;
      if (!notExistingRedirectCellAdded) {
        row.innerHTML += notExistingRedirectCell;
        notExistingRedirectCellAdded = true;
      }
      tableBody.appendChild(row);
    }
  };

  // src/Utilities/CssInjector.ts
  var injectCssCode = (cssCode) => {
    const styleElement = document.createElement("style");
    styleElement.innerHTML = cssCode;
    document.head.appendChild(styleElement);
  };

  // src/Styles/WikiTable.css
  var WikiTable_default = "/* Sadly, the Template:Yes and Template:No use inline styles and not CSS classes, so we need to include\n our own stylesheet. Besides, there is no Template:Blue */\n\n.wikitable {\n    .green-link {\n        background: #afa;\n    }\n    .blue-link {\n        background: #aaf;\n    }\n    .red-link {\n        background: #faa;\n    }\n    .muted-link {\n        background: #d6d6d6;\n        text-align: center;\n        vertical-align: center;\n        font-size: .7rem;\n    }\n}";

  // src/Styles/Common.css
  var Common_default = ".hidden {\n    display: none;\n}";

  // src/index.ts
  globalThis.getMwApi = getMwApi;
  globalThis.getId = getCachedPageInfo;
  globalThis.setId = setCachedPageInfo;
  globalThis.getContent = getCurrentPageContent;
  var runEditHook = true;
  setupDb().then(() => {
    console.debug("ArchTranslator: setupDb successful");
    const manager = new InjectionManager();
    manager.on(0 /* DocumentLoad */, () => {
      console.debug("Document loaded");
      injectCssCode(Common_default);
      injectCssCode(WikiTable_default);
    });
    manager.on(1 /* MediaWikiStartup */, () => {
      const api = getMwApi();
      console.debug(api.config.values.wgTitle);
      cacheCurrentPage().then(() => {
        console.debug("index (MediaWikiStartup): cached current page");
      });
      const pageInfo = getCurrentPageInfo();
      for (const tool of allSidebarTools) {
        ToolManager.instance.addSidebarTool(tool, pageInfo);
      }
      ToolManager.instance.addSidebarToPage();
    });
    manager.onEditForm(async (form) => {
      if (!runEditHook) return;
      console.debug("editform hook");
      const codeMirrorElement = form.find(".CodeMirror");
      if (codeMirrorElement.length > 0) {
        console.debug("CodeMirror found");
        runEditHook = false;
        const cmEditor = codeMirrorElement.get()[0].CodeMirror;
        cacheCurrentPageContent(cmEditor.getValue());
        const pageInfo = getCurrentPageInfo();
        if (pageInfo.pageType === 3 /* CreateEditor */ || pageInfo.pageType === 2 /* Editor */) {
          const info = getCurrentPageInfo();
          if (!info.isTranslated) return;
          addTranslatedArticlesUi(form);
          const englishName = removeLanguagePostfix(info.pageName);
          const englishContent = await getPageContent(englishName);
          await setCachedPageInfo({
            pageName: englishName,
            type: "english" /* English */,
            latestRevisionId: englishContent.revisionId
          });
          const contentToParse = pageInfo.pageType === 3 /* CreateEditor */ ? englishContent.content : cmEditor.getValue();
          const parser = new WikiTextParser();
          parser.parse(contentToParse);
          const newTranslationWorker = new NewArticleWorker(
            pageInfo,
            await getCurrentLanguage(),
            englishContent.revisionId
          );
          const translatedArticleWorker = new TranslatedArticlesWorker(pageInfo);
          translatedArticleWorker.run(parser).then((r) => {
            console.debug(r);
            console.debug("Translated articles worker done");
            addWorkerResultToUi(r);
          });
          if (pageInfo.pageType === 3 /* CreateEditor */) {
            newTranslationWorker.run(parser);
            const newContent = parser.pageContent;
            cacheCurrentPageContent(newContent);
            cmEditor.setValue(newContent);
          }
        }
      }
    });
    manager.startAgents();
  }).catch((e) => {
    console.debug("ArchTranslator: an error has occurred while setting up the database");
    console.error(e);
  });
})();