Middawaida:YMS/eagleeye.js

Vun Wikipedia

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
/**
 * EagleEye
 * Tool for searching, spotting and correcting hard-to-sport errors
 * For documentation see [[de:User:YMS/EagleEye]]
 * <nowiki> ([[bugzilla:8761]])
 */
(function() {
  // JavaScript strict mode
  "use strict";

  // Options and Ruleset
  var ruleset = getOption("eeRuleset", mw.config.get("wgFormattedNamespaces")[2] + ":" + mw.config.get("wgUserName") + "/eagleeye.ruleset.js"); // The ruleset file (using a .js file in your user space prevents others from changing your rules)
  var lang = getOption("eeLang", getDefaultLang(["en", "de"])); // Language (currently supported: de, en; default: user language or project language or en, whichever first is available)
  var activated = getOption("eeActivate", [true, true, true]); // Activate scanner (portlet and tool itself) / marker / corrector (button and script itself)
  var namespaces = getOption("eeNamespaces", [[0], [0], [0]]); // Namespaces for scanner (default value only) / marker / corrector
  var useSkiplist = getOption("eeUseSkiplist", [true, false, false]); // Whether the skiplist should be used for scanner / marker / corrector ('''NOTE: not yet implemented for marker/corrector''')
  var mPrefix = getOption("eeMarkerPrefix", "ee_"); // Prefix for markers (e.g. "ee_" will result in markers like "ee_Doppelwort", may be set to "" for no prefix)
  var mStyle = getOption("eeMarkerStyle", "background-color: #FF9191;"); // CSS style for the marker highlighting (more can be defined for span.eeMarker in user CSS)
  var sChunkSize = getOption("eeScannerChunkSize", 10000000); // The chunk size for the database scanner (too low values will fail [depends on database] or cause bad performance, too high values may cause bad performance or even crashes)
  var sLimit = getOption("eeScannerLimit", 0); // The maximum number of results scanner should find (0 for no limit)
  var sListAll = getOption("eeScannerListAll", false); // Whether the scanner should list all matches for a page instead of only the first one
  var sListReplacements = getOption("eeScannerListReplacements", false); // Whether the scanner should list replacements, too
  var sPage = getOption("eeScannerPage", mw.config.get("wgFormattedNamespaces")[2] + ":YMS/EagleEye"); // The page that will trigger the database scanner (usually only useful where User:YMS/EagleEye doesn't exist)
  var sResultPage = getOption("eeScannerResultPage", mw.config.get("wgFormattedNamespaces")[2] + ":" + mw.config.get("wgUserName") + "/eagleeye.result"); // The default page where result lists are saved (on demand)
  var sSaveWithMatches = getOption("eeScannerResultPageWithMatches", true); // Whether saving the result page should include matches (instead of titles only)
  var sIgnoreRedirects = true; // With current scanner functionality, it's pointless to allow redirects

  // Labels
  var labels = {
    genArticleNS: { en: "(Article)", de: "(Artikel)" },
    genName: { en: "EagleEye", de: "EagleEye" },
    genStartScanner: { en: "Start EagleEye database scanner", de: "EagleEye Datenbankscanner starten" },
    genUnknownError: { en: "Unknown error. ", de: "Unbekannter Fehler." },
    intLabelMissing: { en: "Internal error: Label missing: {0}", de: "Interner Fehler: Label {0} fehlt" },
    lblAbort: { en: "Abort scan", de: "Scan abbrechen" },
    lblAddRule: { en: "Add new rule", de: "Neue Regel hinzufügen" },
    lblCancel: { en: "cancel", de: "abbrechen" },
    lblChunkSize: { en: "Chunk size (in bytes)", de: "Chunk-Größe (in Byte)" },
    lblInstalled: { en: "Script already is installed and active.", de: "Das Skript ist bereits installiert und aktiv." },
    lblLimit: { en: "Limit results (0 = no limit)", de: "Ergebnisse begrenzen (0 = keine Begrenzung)" },
    lblListAll: { en: "List all matches (instead of only the first one for every page)", de: "Alle Ergebnisse auflisten (statt nur dem ersten pro Seite)" },
    lblListReplacements: { en: "Also list replacements (slower search, but better overview and possibility to preview Corrector changes)", de: "Auch Ersetzungen auflisten (langsamere Suche, aber besser Übersicht und Vorschaumöglichkeit für Corrector-Änderungen)" },
    lblMatch: { en: "Match", de: "Match" },
    lblName: { en: "Name", de: "Name" },
    lblNamespaces: { en: "Namespaces", de: "Namensräume" },
    lblNotes: { en: "Notes", de: "Anmerkungen" },
    lblOptions: { en: "Options", de: "Optionen" },
    lblProgress: { en: "{0}% ({1} of {2} bytes) scanned", de: "{0}% ({1} von {2} Bytes) gescannt" },
    lblRemoveRule: { en: "remove", de: "löschen" },
    lblRemoveRuleLong: { en: "Remove rule", de: "Regel löschen" },
    lblRemoveRuleLongest: { en: "Delete rule {0}?", de: "Regel {0} löschen?" },
    lblReplace: { en: "Replace", de: "Ersetze" },
    lblRule: { en: "Rule", de: "Regel" },
    lblRules: { en: "Rules", de: "Regeln" },
    lblSaveResultsWMatches: { en: "include matches (instead of page list only)", de: "inkl. Matches (statt nur einer Liste der Seiten)" },
    lblSaveResults: { en: "Save Results", de: "Ergebnisse speichern" },
    lblSaveResultsOnPage: { en: "on page", de: "auf Seite" },
    lblSaveRules: { en: "Save Ruleset", de: "Ruleset speichern" },
    lblSaveRulesInfo: { en: "Attention: By this, you will perform an edit of page [[{0}}]], overwriting it completely.", de: "Achtung: Damit bearbeitest und überschreibst du die Seite [[{0}]]." },
    lblScanDump: { en: "Scan dump", de: "Dump scannen" },
    lblSearch: { en: "Search", de: "Suche" },
    lblSelectAllNamespaces: { en: "Select/deselect all", de: "Alle/keinen auswählen" },
    lblSelectAllRules: { en: "Activate/deactivate all rules", de: "Alle/keine Regel aktivieren" },
    lblSelectDump: { en: "Select database dump file", de: "Datenbank-Dump-Datei wählen" },
    lblSkip: { en: "Skip", de: "Überspringen" },
    lblTest: { en: "Test", de: "Test" },
    lblTitle: { en: "Title", de: "Titel" },
    lblUseSkiplist: { en: "Use Skiplist", de: "Skiplist verwenden" },
    saveRuleFailed: { en: "Error saving ruleset: {0}", de: "Fehler beim Speichern des Rulesets: {0}" },
    saveRuleSuccess: { en: "Ruleset successfully saved.", de: "Ruleset erfolgreich gespeichert." },
    saveRuleSummary: { en: "Save [[{0}|EagleEye]] ruleset changes", de: "[[{0}|EagleEye]]-Ruleset-Änderungen gespeichert" },
    saveResultsFailed: { en: "Error saving result list: {0}", de: "Fehler beim Speichern der Ergebnisliste: {0}" },
    saveResultsSuccess: { en: "Result list successfully saved.", de: "Ergebnisliste erfolgreich gespeichert." },
    saveResultsSummary: { en: "Save [[{0}|EagleEye]] result list", de: "[[{0}|EagleEye]]-Ergebnisliste gespeichert" },
    statAborted: { en: "Scan aborted. Found {0} pages in {1}.", de: "Scan abgebrochen. {0} Seiten in {1} gefunden." },
    statCheckingRules: { en: "Checking Rules...", de: "Prüfe Regeln..." },
    statChunkTooSmall: { en: "Error: Chunk size too small to process dump.", de: "Fehler: Chunk-Größe zu klein für diesen Dump." },
    statFinished: { en: "Finished. Found {0} pages in {1}.", de: "Fertig. {0} Seiten in {1} gefunden." },
    statInvalidChunkSize: { en: "Error: Chunk size invalid.", de: "Fehler: Chunk-Größe ungültig." },
    statInvalidLimit: { en: "Error: Result limit invalid.", de: "Fehler: Ergebnis-Limit ungültig." },
    statJSONfailed: { en: "Error: EagleEye Ruleset {0} could not be loaded: {1}", de: "Fehler: EagleEye-Ruleset {0} konnte nicht geladen werden: {1}" },
    statNoFile: { en: "Error: No file selected.", de: "Fehler: Keine Datei ausgewählt." },
    statRuleFailsTest: { en: "Error: Rule {0} ({1}) fails defined test.", de: "Fehler: Regel {0} ({1}) besteht den angegebenen Test nicht." },
    statRuleInvalid: { en: "Error: Rule {0} ({1}) invalid: {2}", de: "Fehler: Regel {0} ({1}) ungültig: {2}" },
    statRulesetMissing: { en: "Error: EagleEye ruleset empty.", de: "Fehler: Leeres EagleEye-Ruleset angegeben." },
    statRuleUndefined: { en: "Error: Rule {0} ({1}) undefined.", de: "Fehler: Regel {0} ({1}) undefiniert." },
    statScanningDump: { en: "Scanning {0} dump.", de: "Scanne Dump {0}." },
    statStartScanning: { en: "Start scanning.", de: "Starte Scanvorgang." },
    statUnsupBrowser: { en: "Error: Unsupported browser.", de: "Fehler: Browser nicht unterstützt." },
    statUnsupDump: { en: "Error: Unsupported dump type.", de: "Fehler: Dump nicht unterstützt." },
    statXRegExpFailed: { en: "Error: XRegExp library could not be loaded: {1}", de: "Fehler: XRegExp-Bibliothek konnte nicht geladen werden: {1}" }
  };

  // Internal variables
  var SCANNER = 0;
  var MARKER = 1;
  var CORRECTOR = 2;
  var rules;
  var base;
  var start = 0;
  var stop = 0;
  var nextText = "";
  var subResultCount;
  var resultList = [];
  var startTime;
  var running;
  var api;
  var colGreen = "#88FF88";
  var colRed = "#FF8888";


  // Startup - load ruleset file
  $(document).ready(function() {
    try {
      $.getScript("http://tools.wmflabs.org/eagleeye/xregexp-all-min.js", function() {
        try {
          XRegExp.install("natives");

          if (getOption("eeRulesetDbg", "").toString().length) {
            // Debug mode - load rules from global JSON var eeRulesetDbg
            init(getOption("eeRulesetDbg", ""));
          } else {
            // Production mode - load rules from specified JSON file
            $.getJSON(mw.config.get("wgServer") + mw.config.get("wgScript") + "?title=" + mw.util.wikiUrlencode(ruleset) + "&action=raw&ctype=application/json").done(function(data) {
              init(data);
            }).fail(function(jqxhr, textStatus, error) {
              mw.notify(getText("statJSONfailed").format(ruleset, textStatus + ": " + error));
            });
          }
        } catch (e) {
          mw.notify(getText("statXRegExpFailed").format(e));
        }
      }).fail(function(jqxhr, textStatus, error) {
        mw.notify(getText("statXRegExpFailed").format(textStatus + ": " + error));
      });
    } catch (e) {

    }
  });



  // String formatter
  String.prototype.format = function() {
    var i;
    var s = this;

    for (i = 0; i < arguments.length; i++) {
      s = XRegExp.replace(s, XRegExp.cache("\\{" + i + "\\}", "gm"), arguments[i]);
    }

    return s;
  };

  // Corrector: Add button to edit mode
  function addCorrectorButton() {
    $("#wpTextbox1").wikiEditor("addToToolbar", {
      section: "main",
      group: "format",
      tools: {
        EagleEyeCorrector: {
          label: getText("genName"),
          type: "button",
          icon: "//upload.wikimedia.org/wikipedia/commons/thumb/f/fb/PR_icon.png/22px-PR_icon.png",
          action: {
            type: "callback",
            execute: function() {
              scanEditWindow(this);
            }
          }
        }
      }
    });
  }

  // Add the findings for the current page to the result list (scanner tool display)
  function addPageResultsToDisplay(page) {
    var pageResults = scanPage(page);

    if (pageResults.matches.length > 0) {
      var rule, i;
      var count = 0;
      var first = true;

      resultList.push(pageResults);

      for (rule in pageResults.matches) {
        count += pageResults.matches[rule].length;
      }

      $("#eeResultTable").append($("<tr />", { id: "eeResult_" + subResultCount, "class": "eeResultTableDataLine" }));
      $("#eeResult_" + subResultCount).append($("<td />", { "class": "eeResultTableTitleColumn", rowspan: count }).append('[[<a href="' + base + mw.util.wikiUrlencode(pageResults.title) + '">' + pageResults.title + '</a>]]'));

      for (rule in pageResults.matches) {
        if (! first) {
          $("#eeResultTable").append($("<tr />", { id: "eeResult_" + subResultCount, "class": "eeResultTableDataLine" }));
        }

        first = false;

        $("#eeResult_" + subResultCount).append($("<td />", { "class": "eeResultTableRuleColumn", rowspan: pageResults.matches[rule].length }).append(rules[rule].name));

        for (i = 0; i < pageResults.matches[rule].length; i++) {
          if (i !== 0) {
            $("#eeResultTable").append($("<tr />", { id: "eeResult_" + subResultCount, "class": "eeResultTableDataLine" }));
          }

          $("#eeResult_" + subResultCount).append($("<td />", { "class": "eeResultTableMatchColumn" }).append(mw.html.escape(pageResults.matches[rule][i])));
          $("#eeResult_" + subResultCount).append($("<td />", { "class": "eeResultTableReplaceColumn" }).append(mw.html.escape(pageResults.replaces[rule][i])));
          subResultCount++;
        }
      }
    }
    return (sLimit === 0 || sLimit > resultList.length);
  }

  // Add results to the results page (wikitext export)
  function addPageResultsToExport(pageResults, addMatches) {
    var rule, i;
    var text = "";

    if (pageResults.matches.length > 0) {
      text = "* [[{0}]]".format(pageResults.title);

      if (addMatches) {
        for (rule in pageResults.matches) {
          var ruleMatches = "";

          for (i = 0; i < pageResults.matches[rule].length; i++) {
            if (i !== 0) {
              ruleMatches += "; ";
            }

            ruleMatches += mw.html.escape(pageResults.matches[rule][i]);
          }

          text += " ({0}: {1})".format(rules[rule].name, ruleMatches);
        }
      }

      text += "\n";
    }

    return text;
  }

  // Add a rule editor for the given line
  function addRuleEditor(i) {
    $("#eeRuleEditorTable").append($("<tr />", { id: "eeRuleEditor_" + i, "class": "eeRuleEditorTableDataLine" }));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableCheckboxColumn" }).append($("<input />", { type: "checkbox", id: "eeRuleCB_" + i, checked: (rules[i].active === true) })));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableNameColumn" }).append($("<input />", { type: "text", id: "eeRuleName_" + i, value: rules[i].name }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableMatchColumn" }).append($("<input />", { type: "text", id: "eeRuleMatch_" + i, value: rules[i].match }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableSkipColumn" }).append($("<input />", { type: "text", id: "eeRuleSkip_" + i, value: rules[i].skip }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableReplaceColumn" }).append($("<input />", { type: "text", id: "eeRuleReplace_" + i, value: rules[i].replace }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableTestColumn" }).append($("<input />", { type: "text", id: "eeRuleTest_" + i, value: rules[i].test }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableNotesColumn" }).append($("<input />", { type: "text", id: "eeRuleNotes_" + i, value: rules[i].note }).css("width", "100%")));
    $("#eeRuleEditor_" + i).append($("<td />", { "class": "eeRuleEditorTableRemoveColumn" }).append($("<input />", { type: "button", id: "eeRuleRemove_" + i, value: getText("lblRemoveRule") })));

    // Check test on relevant changes
    $("#eeRuleMatch_" + i + ",#eeRuleTest_" + i).change(function() {
      if (applyRuleSettingsFromUI(i)) {
        $("#eeRuleTest_" + i).css("background-color", (validateRule(i)) ? colGreen : colRed);
      }
    }).change();

    // Button "remove"
    $("#eeRuleRemove_" + i).click(function() {
      applyRuleSettingsFromUI(i);

      showConfirm(getText("lblRemoveRuleLong"), getText("lblRemoveRuleLongest").format(rules[i].name), getText("lblRemoveRule"), getText("lblCancel"), function () {
        rules.splice(i, 1);
        loadRuleEditor();
      });
    });
  }

  // Add a collapsible section to the HTML
  function addUISection(sectionID, bodyID, sectionContent, bodyContent, collapsed) {
    $("#mw-content-text").append($("<div/>", { id: sectionID, "class": "mw-collapsible eeSection" }).addClass((collapsed) ? "mw-collapsed" : "").append($("<b />").append(sectionContent)).append($("<div />", { id: bodyID, "class": "mw-collapsible-content" }).append(bodyContent)));
  }

  // Get the settings for a certain rule from user's input
  function applyRuleSettingsFromUI(i) {
    try {
      rules[i].active = $("#eeRuleCB_" + i).prop("checked");
      rules[i].name = $("#eeRuleName_" + i).val();
      rules[i].match = $("#eeRuleMatch_" + i).val();
      rules[i].skip = $("#eeRuleSkip_" + i).val();
      rules[i].replace = $("#eeRuleReplace_" + i).val();
      rules[i].test = $("#eeRuleTest_" + i).val();
      rules[i].note = $("#eeRuleNotes_" + i).val();
    } catch (e) {
      return false;
    }

    return true;
  }

  // Get rules and other settings from user's input
  function applySettingsFromUI() {
    var i;

    // Rules
    for (i = 0; i < rules.length; i++) {
      if (! applyRuleSettingsFromUI(i)) {
        setStatus(getText("statRuleInvalid").format(i, rules[i].name, getText("genUnknownError")), false);
        return;
      }
    }

    // Options
    useSkiplist[SCANNER] = $("#eeOptionCB_useSkiplist").prop("checked");
    sListAll = $("#eeOptionCB_listAll").prop("checked");
    sListReplacements = $("#eeOptionCB_listReplacements").prop("checked");

    sChunkSize = Number($("#eeOptionCB_chunkSize").val());
    if (! $.isNumeric(sChunkSize) || sChunkSize <= 0) {
      setStatus(getText("statInvalidChunkSize"), false);
      return;
    }

    sLimit = Number($("#eeOptionCB_limit").val());
    if (! $.isNumeric(sLimit) || sLimit < 0) {
      setStatus(getText("statInvalidLimit"), false);
      return;
    }

    // Namespaces
    namespaces[SCANNER] = [];
    $("input[id^=eeNamespaceCB_]").each(function() {
      if ($(this).prop("checked")) {
        namespaces[SCANNER].push(Number($(this).attr("id").substring($(this).attr("id").indexOf("_") + 1)));
      }
    });
  }

  // Save page via API
  function editPage(title, text, summary, successMsg, failMsg) {
    api.post({ action: "edit", title: title, text: text, summary: summary, token: mw.user.tokens.get("csrfToken") }).done(function(data) {
      if (data && data.edit && data.edit.result === "Success") {
        mw.notify(successMsg);
      } else if (data && data.error) {
        mw.notify(failMsg.format(data.error.code + ": " + data.error.info));
      } else {
        mw.notify(failMsg.format(getText("genUnknownError")));
      }
    }).fail(function(error) {
      mw.notify(failMsg.format(error));
    });
  }

  // Enable/disable the "Save Results" button
  function enableSaveResultsButton() {
    var enable = ! running && true && resultList.length > 0;
    $("#eeSaveResults").attr("disabled", ! enable);
  }

  // Enable/disable the "Save Rules" button
  function enableSaveRulesButton() {
    var enable = true && rules.length > 0;
    $("#eeSaveRules").attr("disabled", ! enable);
  }

  // Enable/disable the "Search/Abort" button
  function enableSearchButton() {
    var enable = document.getElementById("eeFile").files.length > 0;
    $("#eeSearch").attr("disabled", ! enable);
  }

  // Get default language (user language, if available, else project language, if available, else first defined language)
  function getDefaultLang(supportedLanguages) {
    if ($.inArray(mw.config.get("wgUserLanguage"), supportedLanguages) !== -1) {
      return mw.config.get("wgUserLanguage");
    } else if ($.inArray(mw.config.get("wgContentLanguage"), supportedLanguages) !== -1) {
      return mw.config.get("wgContentLanguage");
    } else {
      return supportedLanguages[0];
    }
  }

  // Get duration of a scan in format mm:ss
  function getDuration() {
    var msec = ($.now() - startTime);
    var sec = ((Math.floor(msec / 1000) % 60 < 10) ? "0" : "") + Math.floor(msec / 1000) % 60;
    var min = ((Math.floor(msec / 1000 / 60) < 10) ? "0" : "") + Math.floor(msec / 1000 / 60);

    return min + ":" + sec;
  }

  // Load a user-defined configuration variable or the default value
  function getOption(name, defaultvalue) {
    return (typeof window[name] === "undefined") ? defaultvalue : window[name];
  }

  // Internationalisation of a label
  function getText(label) {
    if (labels[label] === null || labels[label][lang] === null) {
      return getText("intLabelMissing").format(label);
    }

    return labels[label][lang];
  }

  // Initialise scanner, marker, corrector and additional stuff (scanner portlet, etc.)
  function init(data) {
    var i;

    if (! window.File || ! window.FileReader || ! window.Blob) {
      setStatus(getText("statUnsupBrowser"), false);
      return;
    }

    rules = data;

    if (typeof rules === "undefined" || rules === null || rules.length === 0) {
      mw.notify(getText("statRulesetMissing"));
      return;
    }

    if (activated[SCANNER]) {
      mw.util.addPortletLink("p-tb", mw.util.getUrl(sPage), getText("genName"), "t-eagleeye", getText("genStartScanner"));

      if (mw.config.get("wgPageName") === sPage && mw.config.get("wgAction") === "view") {
        // Scanner view
        mw.util.addCSS("div.eeSection { border: 1px solid black; padding: 8px; }");
        mw.util.addCSS(".eeResultTable td { border-top: 1px solid #CCCCCC; padding: 4px }");
        mw.util.addCSS("tr.eeRuleEditorTableDataLine:hover { background-color: #CCCCCC }");

        mw.loader.using([ "mediawiki.api" ], function() {
          api = new mw.Api();
        });

        loadScannerUI();
      }
    }

    if (activated[MARKER] && $.inArray(mw.config.get("wgNamespaceNumber"), namespaces[MARKER]) !== -1 && mw.config.get("wgAction") === "view") {
      // Article view: Marker
      mw.util.addCSS("span.eeMarker { " + mStyle + " }");

      scanView();
    }

    if (activated[CORRECTOR] && $.inArray(mw.config.get("wgNamespaceNumber"), namespaces[CORRECTOR]) !== -1 && $.inArray(mw.config.get("wgAction"), [ "edit", "submit" ]) !== -1) {
      // Edit view - add button for Corrector
      mw.loader.using("ext.wikiEditor", function () {
        addCorrectorButton();
      });
    }
  }

  // (Re)loads the rule editor UI
  function loadRuleEditor() {
    var i;

    var table = $("<table />", { id: "eeRuleEditorTable", "class": "eeRuleEditorTable", width: "100%" });
    var header = $("<tr />", { id: "eeRuleEditorTableHeaderLine", "class": "eeRuleEditorTableHeaderLine" });
    table.append(header);
    header.append($("<th />", { "class": "eeRuleEditorTableCheckboxColumn" }));
    header.append($("<th />", { "class": "eeRuleEditorTableNameColumn", width: "5%" }).append(getText("lblName")));
    header.append($("<th />", { "class": "eeRuleEditorTableMatchColumn", width: "40%" }).append(getText("lblMatch")));
    header.append($("<th />", { "class": "eeRuleEditorTableSkipColumn", width: "30%" }).append(getText("lblSkip")));
    header.append($("<th />", { "class": "eeRuleEditorTableReplaceColumn", width: "5%" }).append($("<i />").append(getText("lblReplace"))));
    header.append($("<th />", { "class": "eeRuleEditorTableTestColumn", width: "10%" }).append($("<i />").append(getText("lblTest"))));
    header.append($("<th />", { "class": "eeRuleEditorTableNotesColumn", width: "10%" }).append($("<i />").append(getText("lblNotes"))));
    header.append($("<th />", { "class": "eeRuleEditorTableCopyColumn" }));

    $("#eeRulesPaneContent").html(table);

    for (i = 0; i < rules.length; i++) {
      addRuleEditor(i);
    }
  }

  // Add the GUI elements for the scanner
  function loadScannerUI() {
    var i, id;

    // Collapse pre-defined installation section and set a marker
    if ($("#eeInstallation").length) {
      $("#eeInstallation").find(".mw-collapsible-content").prepend($("<div />", { id: "eeInstallationNotice" }).append(getText("lblInstalled")));
      $("#eeInstallationNotice").css({ "background-color": "#44DD44", "padding": "5px", "margin": "5px" });
      $("#eeInstallation").addClass("mw-collapsed");
    }

    addUISection("eeRulesPane", "eeRulesPaneContent", getText("lblRules"), "", true);
    loadRuleEditor();

    // Buttons
    $("#eeRulesPane").append($("<div />", { "class": "mw-collapsible-content" }).append($("<input />", { type: "button", id: "eeSelectAllRules", name: "eeSelectAllRules", val: getText("lblSelectAllRules") })).append($("<input />", { type: "button", id: "eeAddRule", name: "eeAddRule", val: getText("lblAddRule") })).append($("<input />", { type: "button", id: "eeSaveRules", name: "eeSaveRules", val: getText("lblSaveRules") })).append($("<i />").append(getText("lblSaveRulesInfo").format(ruleset))));

    $("#eeSelectAllRules").click(function() {
      selectDeselectAll($("input[id^=eeRuleCB_]"));
    });

    $("#eeAddRule").click(function() {
      // Add new (empty) rule to the rule editor
      rules.push({ active: true });
      addRuleEditor(rules.length - 1);
    });

    $("#eeSaveRules").click(function() {
      saveRuleset();
    });

    // Options
    addUISection("eeOptionsPane", "eeOptionsPaneContent", getText("lblOptions"), null, true);

    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeOptionCB_useSkiplist", checked: (useSkiplist[SCANNER]) })).append($("<label />", { "for": "eeOptionCB_useSkiplist" }).append(getText("lblUseSkiplist"))));
    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeOptionCB_listAll", checked: (sListAll) })).append($("<label />", { "for": "eeOptionCB_listAll" }).append(getText("lblListAll"))));
    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeOptionCB_listReplacements", checked: (sListReplacements) })).append($("<label />", { "for": "eeOptionCB_listReplacements" }).append(getText("lblListReplacements"))));
    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "number", id: "eeOptionCB_chunkSize", value: sChunkSize, min: "1" })).append($("<label />", { "for": "eeOptionCB_chunkSize" }).append(getText("lblChunkSize"))));
    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "number", id: "eeOptionCB_limit", value: sLimit, min: "0" })).append($("<label />", { "for": "eeOptionCB_limit" }).append(getText("lblLimit"))));

    // Namespaces
    addUISection("eeNamespacePane", "eeNamespacePaneContent", getText("lblNamespaces"), $("<div />", { id: "eeNamespacesList" }).css("column-width", "200px"), true);

    for (id in mw.config.get("wgFormattedNamespaces")) {
      var name = (mw.config.get("wgFormattedNamespaces")[id] === "") ? getText("genArticleNS") : mw.config.get("wgFormattedNamespaces")[id];
      $("#eeNamespacesList").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeNamespaceCB_" + id, checked: ($.inArray(Number(id), namespaces[SCANNER]) !== -1) })).append($("<label />", { "for": "eeNamespaceCB_" + id }).append(name)));
    }

    $("#eeNamespacePaneContent").append($("<div />").append($("<input />", { type: "button", id: "eeSelectAllNamespaces", name: "eeSelectAllNamespaces", val: getText("lblSelectAllNamespaces") })));

    $("#eeSelectAllNamespaces").click(function() {
      selectDeselectAll($("input[id^=eeNamespaceCB_]"));
    });

    // Search
    addUISection("eeSearchPane", "eeSearchPaneContent", getText("lblSearch"), $("<label />", { "for": "eeFile" }).append(getText("lblSelectDump")));
    $("#eeSearchPaneContent").append($("<div />").append($("<input />", { type: "file", id: "eeFile", name: "eeFile" }), false));
    $("#eeSearchPaneContent").append($("<div />").append($("<input />", { type: "button", id: "eeSearch", name: "eeSearch" })));

    // Output section
    $("#eeSearchPaneContent").append($("<div/>", { id: "eeStatus" }));
    $("#eeSearchPaneContent").append($("<progress />", { id: "eeProgressBar", value: "0" }).css("width", "100%"));
    $("#eeSearchPaneContent").append($("<div/>", { id: "eeProgress" }));
    $("#eeSearchPaneContent").append($("<br/>"));
    $("#eeSearchPaneContent").append($("<output/>", { id: "eeResults" }));
    $("#eeSearchPaneContent").append($("<br/>"));

    // Save button
    $("#eeSearchPaneContent").append($("<div />").append($("<input />", { type: "button", id: "eeSaveResults", name: "eeSaveResults", value: getText("lblSaveResults") })).append(getText("lblSaveResultsOnPage")).append($("<input />", { type: "text", id: "eeSaveResultsPage", val: sResultPage })).append($("<input />", { type: "checkbox", id: "eeSaveResultPageWithMatches", checked: sSaveWithMatches })).append($("<label />", { "for": "eeSaveResultPageWithMatches" }).append(getText("lblSaveResultsWMatches"))));

    // Assure collapsible sections are made collapsible
    mw.loader.using("mediawiki.page.ready", function () {
      //if ($("#eeInstallation").length) {
      // $("#eeInstallation").collapse("mw-collapsed");
      //}

      $(".mw-collapsible").makeCollapsible();
    });

    // File select: Enable Search
    $("#eeFile").change(function() {
      enableSearchButton();
    });

    // Button: "Search"/"Abort"
    $("#eeSearch").click(function() {
      if (running) {
        setStatus(getText("statAborted").format(resultList.length, getDuration()), false);
      } else {
        applySettingsFromUI();
        scanFile();
      }

      setRunning(! running);
    });

    // Button: "Save Results"
    $("#eeSaveResults").click(function() {
      var i;
      var text = "";
      var addMatches = $("#eeSaveResultPageWithMatches").prop("checked");

      for (i = 0; i < resultList.length; i++) {
        text += addPageResultsToExport(resultList[i], addMatches);
      }

      editPage($("#eeSaveResultsPage").val(), text, getText("saveResultsSummary").format($("#eeSaveResultsPage").val()), getText("saveResultsSuccess"), getText("saveResultsFailed"));
    });

    enableSearchButton();
    enableSaveRulesButton();
    setRunning(false);
  }

  // Finished reading a dump chunk, start reading next one
  function readNextChunk(e, reader, file) {
    if (! running || e.target.readyState !== FileReader.DONE || file === null || ! file) {
      setRunning(false);
      return;
    }

    setProgress(stop, file.size);

    // Trim chopped-of tags
    var text = e.target.result;

    if (start > 0) {
      text = "<mediawiki>" + nextText + text;
    }

    var lastCloseText = text.lastIndexOf("</page>");

    if (lastCloseText < 0) {
      setStatus(getText("statChunkTooSmall"), false);
      return;
    }

    nextText = text.substring(lastCloseText + "</page>".length);
    text = text.substring(0, lastCloseText + "</page>".length) + "</mediawiki>";

    // Detect base
    if (start === 0) {
      // jQuery can't read tags named "base" for some reason, so load this manually
      var baseStart = text.indexOf("<base>");
      base = text.substring(baseStart + "<base>".length, text.indexOf("</base>"));
      base = base.substring(0, base.lastIndexOf("/") + 1);

      if (baseStart < 0 || base.length === 0) {
        setStatus(getText("statUnsupDump"), false);
        return;
      }

      setStatus(getText("statScanningDump").format(base), true);
    }

    // Search
    $(text).find("page").each(function() {
      // Check namespace
      if ($.inArray(Number($(this).find("ns").text()), namespaces[SCANNER]) === -1) {
        return;
      }

      // Check redirect status
      if (sIgnoreRedirects && $(this).find("redirect").length > 0) {
        return;
      }

      return addPageResultsToDisplay($(this));
    });

    // Next chunk
    if (stop < file.size && (sLimit === 0 || sLimit > resultList.length)) {
      start = start + sChunkSize;
      stop = stop + sChunkSize;

      reader.readAsText(file.slice(start, stop));
    } else {
      setStatus(getText("statFinished").format(resultList.length, getDuration()), true);
      setRunning(false);
    }
  }

  // Save ruleset definition to specified file
  function saveRuleset() {
    var i;

    // Convert rules to JSON
    for (i = 0; i < rules.length; i++) {
      if (! applyRuleSettingsFromUI(i)) {
        mw.notify(getText("statRuleInvalid").format(i, rules[i].name, getText("genUnknownError")));
        return;
      }
    }

    if (! validateRuleset()) {
      mw.notify($("#eeStatus").text());
      return;
    }

    // Save rules
    editPage(ruleset, JSON.stringify(rules, null, 2), getText("saveRuleSummary").format(sPage), getText("saveRuleSuccess"), getText("saveRuleFailed"));
  }

  // Corrector: Edit window processing
  function scanEditWindow(context) {
    var i;
    var changed = false;
    var a = context.clickedElement;

    for (i = 0; i < rules.length; i++) {
      if (rules[i].active === true) {
        if (! changed && $("#wpTextbox1").text().replace(XRegExp.cache(rules[i].match, "g")) !== null) {
          changed = true;
        }

      $("#wpTextbox1").text(XRegExp.replace($("#wpTextbox1").text(), XRegExp.cache(rules[i].match, "g"), rules[i].replace));
      }
    }

    if (! a || ! a.nodeType || a.nodeName === "IMG") {
      $((a && a.nodeType) ? a : "img[rel=EagleEyeCorrector]").css("background-color", (changed) ? colGreen : colRed);
    }
  }

  // Perform dump scan
  function scanFile() {
    var file = document.getElementById("eeFile").files[0];

    start = 0;
    stop = start + sChunkSize;

    $("#eeResults").html($("<table />", { id: "eeResultTable", "class": "eeResultTable" }));
    var header = $("<tr />", { id: "eeResultTableHeaderLine", "class": "eeResultTableHeaderLine" });
    $("#eeResultTable").append(header);
    header.append($("<th />", { "class": "eeResultTableTitleColumn" }).append(getText("lblTitle")));
    header.append($("<th />", { "class": "eeResultTableRuleColumn" }).append(getText("lblRule")));
    header.append($("<th />", { "class": "eeResultTableMatchColumn" }).append(getText("lblMatch")));
    header.append($("<th />", { "class": "eeResultTableReplaceColumn" }).append(getText("lblReplace")));

    setStatus(getText("statCheckingRules"), true);
    setProgress(0, file.size);
    resultList = [];
    subResultCount = 0;
    startTime = $.now();

    if (! validateRuleset()) {
      return;
    }

    setStatus(getText("statStartScanning"), true);

    if (file === null || ! file) {
      setStatus(getText("statNoFile"), false);
      return;
    }

    var reader = new FileReader();
    reader.onloadend = function(e) {
      readNextChunk(e, reader, file);
    };
    reader.readAsText(file.slice(start, stop));
  }

  // Iterate all rules and match given text to it
  function scanPage(object) {
    var i, j;
    var pageResults = { title: object.find("title").text(), matches: [], replaces: [] };
    for (i = 0; i < rules.length; i++) {
      if (rules[i].active === true) {
        // Match rule if active
        var match = object.find("text").text().match(XRegExp.cache(rules[i].match, "g"));

        if (match !== null) {
          // Check results for skiplist match
          for (j = 0; j < match.length; j++) {
            if (! useSkiplist[SCANNER] || rules[i].skip === undefined || rules[i].skip === XRegExp.cache("") || rules[i].skip.length === 0 || ! match[j].match(XRegExp(rules[i].skip))) {
              var replacement = (sListReplacements) ? XRegExp.replace(match[j], XRegExp.cache(rules[i].match, "g"), rules[i].replace, "one") : "";

              if (pageResults.matches[i] === undefined) {
                pageResults.matches[i] = [match[j]];
                pageResults.replaces[i] = [replacement];
              } else {
                pageResults.matches[i].push(match[j]);
                pageResults.replaces[i].push(replacement);
              }

              if (! sListAll) {
                return pageResults;
              }
            }
          }
        }
      }
    }

    return pageResults;
  }

  // Marker: Mark findings in article view
  function scanView() {
    var i;

    $("#mw-content-text").children(":not(.diff):not(.diff *)").each(function() {
      if ($(this).html() !== null) {
        var text = $(this).html();

        for (i = 0; i < rules.length; i++) {
          if (rules[i].active === true) {
            text = text.replace(XRegExp.cache(rules[i].match, "g"), '<span class="eeMarker">$&<sup>' + mPrefix + rules[i].name + '</sup></span>');
          }
        }

        $(this).html(text);
      }
    });
  }

  // Select or deselect all of the given checkboxes
  function selectDeselectAll($boxes) {
      var allActivated = true;

      $boxes.each(function() {
        if (! $(this).prop("checked")) {
          allActivated = false;
          return false;
        }
      });

      $boxes.prop("checked", ! allActivated);
  }

  // Sets text of progress display and bar
  function setProgress(value, max) {
    if (value === 0) {
      $("#eeProgress").text("");
      $("#eeProgressBar").attr({ value: value, max: max });
    } else {
      var percentage = (Math.min(value, max) / max) * 100;
      $("#eeProgress").text(getText("lblProgress").format(percentage.toFixed(0), Math.min(value, max), max));
      $("#eeProgressBar").attr("value", value);
    }
  }

  // Sets the running state and changes start/abort button's label and behaviour
  function setRunning(run) {
    running = run;

    if (running) {
      $("#eeSearch").val(getText("lblAbort"));
      $("#eeProgressBar").show();
    } else {
      $("#eeSearch").val(getText("lblScanDump"));
      $("#eeProgressBar").hide();
    }

    enableSaveResultsButton();
  }

  // Sets text of status section
  function setStatus(text, okay) {
    $("#eeStatus").text(text);
    $("#eeStatus").css("background-color", (okay) ? colGreen : colRed);
  }

  // Display a message box with two options
  function showConfirm(title, text, doText, cancelText, callback) {
    $("<div />").dialog({
      title: title,
      modal: true,
      buttons: [{
        text: doText,
        click: function() {
          callback();
          $(this).remove();
        }
      }, {
        text: cancelText,
        click: function() {
          $(this).remove();
        }
      }],
      close: function (event, ui) {
        $(this).remove();
      }
    }).text(text).parent().addClass("alert");
  }

  // Validate a single RegEx rule
  function validateRule(i) {
    if (rules[i].match === null) {
      setStatus(getText("statRuleUndefined").format(i, rules[i].name), false);
      return false;
    }

    try {
      if (rules[i].test !== null && rules[i].test.length > 0) {
        // Check standard test case
        if (rules[i].test.match(XRegExp(rules[i].match, "g")) === null) {
          setStatus(getText("statRuleFailsTest").format(i, rules[i].name), false);
          return false;
        } else {
          setStatus("", true);
        }
      } else {
        // Perform dummy test to check formal validity
        XRegExp.test("dummy", rules[i].match);
      }
    } catch (e) {
      setStatus(getText("statRuleInvalid").format(i, rules[i].name, e), false);
      return false;
    }

    return true;
  }

  // Validate RegEx rule set
  function validateRuleset() {
    var i;

    for (i = 0; i < rules.length; i++) {
      if (! validateRule(i)) {
        return false;
      }
    }

    return true;
  }
})();
// </nowiki>