Unnaschied vunde Gschischde vun "Middawaida:YMS/eagleeye.js"

Inhalt gelöscht Inhalt hinzugefügt
YMS (Dischbediere | Baidräsch)
EagleEye v0.1
(kän Unnaschied)

Iwwa'awaidung vun 15:45, 25. Mai 2013

/**
 * EagleEye
 * Tool for searching, spotting and correcting hard-to-sport errors
 * For more detailed documentation see [[de:User:YMS/EagleEye]]
 * <nowiki> ([[bugzilla:8761]])
 */
(function() {
  // Options and Ruleset
  var rules = getOption("eeRules", []); // The rule set
  var lang = getOption("eeLang", "en"); // Language (currently supported: en)
  var activateScanner = getOption("eeActivateScanner", true); // Activate database scanner (portlet and tool itself)
  var activateScript = getOption("eeActivateScript", true); // Activate correction script (button and script itself)
  var activateMarker = getOption("eeActivateMarker", true); // Activate marker tool
  var namespaces = getOption("eeNamespaces", [0]); // Namespaces for marker and correction script
  var useSkiplist = getOption("eeUseSkiplist", true); // Whether the skiplist should be used for the database scanner
  var chunkSize = getOption("eeChunkSize", 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 markerStyle = getOption("eeMarkerStyle", "background-color: #FF9191;"); // CSS style for the marker highlighting (more can be defined for span.eeMarker in user CSS
  var markerPrefix = getOption("eeMarkerPrefix", "ee_"); // Prefix for markers (e.g. "ee_" will result in markers like "ee_Doppelwort", may be set to "" for no prefix)
  var scannerPage = getOption("eeScannerPage", mw.config.get("wgFormattedNamespaces")[2] + ":YMS/EagleEye"); // The page that will trigger the database scanner
  var ignoreRedirects = true; // With current functionality, it's pointless to allow redirects

  // Labels
  var labels = {
    generalArticleNamespace: { en: "(Article)" },
    generalName: { en: "EagleEye" },
    generalStartScanner: { en: "Start EagleEye database scanner" },
    internalLabelMissing: { en: "Internal error: Label missing: {0}" },
    statusAborted: { en: "Scan aborted." },
    statusCheckingRules: { en: "Checking Rules..." },
    statusChunkSizeTooSmall: { en: "Error: Chunk size too small to process dump." },
    statusFinished: { en: "Finished. Found {0} pages in {1}:{2}." },
    statusInvalidChunkSize: { en: "Error: Chunk size invalid." },
    statusNoFile: { en: "Error: No file selected." },
    statusRuleFailsTest: { en: "Error: Rule {0} ({1}) fails defined test." },
    statusRuleInvalid: { en: "Error: Rule {0} ({1}) invalid: {2}"},
    statusRulesetMissing: { en: "Error: Ruleset missing." },
    statusRuleUndefined: { en: "Error: Rule {0} ({1}) undefined." },
    statusScanningDump: { en: "Scanning {0} dump." },
    statusStartScanning: { en: "Start scanning." },
    statusUnsupportedBrowser: { en: "Error: Unsupported browser." },
    statusUnsupportedDump: { en: "Error: Unsupported dump type." },
    lblAbort: { en: "Abort scan" },
    lblNamespaces: { en: "Namespaces" },
    lblOptions: { en: "Options" },
    lblRules: { en: "Rules" },
    lblScanDump: { en: "Scan dump" },
    lblSearch: { en: "Search" },
    lblSelectDump: { en: "Select database dump file" },
  };
  
  // Internal variables
  var base;
  var start = 0;
  var stop = 0;
  var nextText = "";
  var results;
  var startTime;
  var aborted = false;


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

    for (var i = 0; i < arguments.length; i++) {
        s = s.replace(new RegExp("\\{" + i + "\\}", "gm"), arguments[i]);
    }
    
    return s;
  };
  
  // Internationalisation of a label
  function txt(label) {
    if (labels[label] == null || labels[label][lang] == null) {
      return txt("internalLabelMissing").format(label);
    }
  
    return labels[label][lang];
  }
  
  // Load a user-defined configuration variable or the given default
  function getOption(name, defaultvalue) {
    return (typeof window[name] === "undefined") ? defaultvalue : window[name];
  }
  
  // Prints a new line in the result section
  function printLine(text) {
    $("#eeResults").append($("<span />").append(text).append("<br />"));
  }
  
  // Sets text of progress display
  function setProgress(text) {
    $("#eeProgress").text(text);
  }
  
  // Sets text of status section
  function setStatus(text) {
    $("#eeStatus").text(text);
  }

  // Add a new finding to the results section
  function addResult(title, rule, match, base) {
    printLine('[[<a href="' + base + encodeURI(title) + '">' + title + '</a>]] (' + rules[rule].name + ': ' + $("<textarea />").html(match).html() + ')');
    
    results++;
  }          
  
  // Iterate all rules and match given text to it
  function searchByRules(object) {
    for (var i = 0; i < rules.length; i++) {
      if (rules[i].inactive == null || rules[i].inactive == false) {
        // Match rule if not inactive
        var match = object.find("text").text().match(new RegExp(rules[i].match.source, "g"));
      
        if (match != null) {
          // Check results for skiplist match
          for (var j = 0; j < match.length; j++) {
            if (! useSkiplist || rules[i].skip === undefined || ! match[j].match(rules[i].skip)) {
              addResult(object.find("title").text(), i, match[j], base);
              return;
            }
          }
        }
      }
    }
  }
  
  // Validate RegEx rule set (and apply activation state)
  function checkRules() {
    for (var i = 0; i < rules.length; i++) {
      // Activate/deactivate rule per user selection
      rules[i].inactive = ! $("#eeRuleCB_" + i).attr("checked");
    
      // Check rule
      if (rules[i].match === null) {
        setStatus(txt("statusRuleUndefined").format(i, rules[i].name));
        return false;
      }

      try {
        if (rules[i].test != null) {
          // Check standard test case
          if (rules[i].test.match(rules[i].match) === null) {
            setStatus(txt("statusRuleFailsTest").format(i, rules[i].name));
            return false;
          }
        } else {
          // Perform dummy test to check formal validity
          "dummy".match(rules[i].match);
        }
      } catch (e) {
        setStatus(txt("statusRuleInvalid").format(i, rules[i].name, e));
        return false;
      }
    }
    
    return true;
  }
  
  // Edit window processing
  function scanEditWindow() {
    var changed = false;
    var a = this.clickedElement;

    for (var i = 0; i < rules.length; i++) {
      if (! changed) {
        if ($("#wpTextbox1").text().match(rules[i].match) != null) {
          changed = true;
        }
      }
    
      $("#wpTextbox1").text($("#wpTextbox1").text().replace(new RegExp(rules[i].match.source, "g"), rules[i].replace));
    }
    
    if (changed && (! a || ! a.nodeType || a.nodeName == "IMG")) {
      $((a && a.nodeType) ? a : "img[rel=" + txt("generalName") + "]").css("backgroundColor", changed ? "#DEF740" : "");
    }
  };

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

  // Mark findings in article view
  function markView() {
    $("#mw-content-text").children(":not(.diff)").each(function() {
      if ($(this).html() != null) {
        for (var i = 0; i < rules.length; i++) {
          if (rules[i].inactive == null || rules[i].inactive == false) {
            $(this).html($(this).html().replace(new RegExp(rules[i].match.source, "g"), '<span class="eeMarker">$&<sup>' + markerPrefix + rules[i].name + '</sup></span>'));
          }
        }
      }
    });
  }
  
  // Add a collapsible section to the HTML
  function addSection(sectionID, bodyID, sectionContent, bodyContent, collapsed) {
    $("#mw-content-text").append($("<div/>", { id: sectionID, class: "mw-collapsible eeSection" }).addClass((collapsed) ? "mw-collapsed" : "").append("<b>" + sectionContent + "</b>").append($("<div />", { id: bodyID, class: "mw-collapsible-content" }).append(bodyContent)));
  }
  
  // Add a rule editor for the given line
  function addRuleEditor(i) {
    var checkbox = $("<input />", { type: "checkbox", id: "eeRuleCB_" + i, checked: (rules[i].inactive == null || rules[i].inactive == false) });
    $("#eeRulesPaneContent").append($("<div/>").append(checkbox).append(rules[i].name));
  }
  
  // Add the GUI elements for the scanner
  function loadScannerUI() {
    // Rules
    addSection("eeRulesPane", "eeRulesPaneContent", txt("lblRules"), null, true);
  
    for (var i = 0; i < rules.length; i++) {
      addRuleEditor(i);
    }
    
    // Options
    addSection("eeOptionsPane", "eeOptionsPaneContent", txt("lblOptions"), null, true);

    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeOptionCB_useSkiplist", checked: (useSkiplist) })).append("use Skiplist"));
    $("#eeOptionsPaneContent").append($("<div/>").append($("<input />", { type: "number", id: "eeOptionCB_chunkSize", value: chunkSize, min: 0 })).append("Chunk size"));
  
    // Namespaces
    addSection("eeNamespacePane", "eeNamespacePaneContent", txt("lblNamespaces"), null, true);
    
    for (var id in mw.config.get("wgFormattedNamespaces")) {
      var name = (mw.config.get("wgFormattedNamespaces")[id] === "") ? txt("generalArticleNamespace") : mw.config.get("wgFormattedNamespaces")[id];
      $("#eeNamespacePaneContent").append($("<div/>").append($("<input />", { type: "checkbox", id: "eeNamespaceCB_" + id, checked: ($.inArray(Number(id), namespaces) != -1) })).append(name));
    }

    // Search
    //addSection("eeSearchPane", "eeSearchPaneContent", txt("lblSearch"), $("<input />", { type: "file", id: "eeFile", name: "eeFile" }), false);
    //$("#eeSearchPaneContent").append($("<input />", { type: "button", id: "eeSearch", name: "eeSearch", value: txt("lblScanDump") }));
    
    addSection("eeSearchPane", "eeSearchPaneContent", txt("lblSearch"), txt("lblSelectDump"));
    $("#eeSearchPaneContent").append($("<div />").append($("<input />", { type: "file", id: "eeFile", name: "eeFile" }), false));
    $("#eeSearchPaneContent").append($("<br/>"));
    $("#eeSearchPaneContent").append($("<div />").append($("<input />", { type: "button", id: "eeSearch", name: "eeSearch", value: txt("lblScanDump") })));
    
    // Output section
    $("#eeSearchPaneContent").append($("<div/>", { id: "eeStatus" }));
    $("#eeSearchPaneContent").append($("<div/>", { id: "eeProgress" }));
    $("#eeSearchPaneContent").append($("<div/>", { id: "eeAbortDiv" }).append($("<a />", { id: "eeAbortLink" }).append(txt("lblAbort"))));
    $("#eeAbortDiv").hide();
    $("#eeSearchPaneContent").append($("<br/>"));
    $("#eeSearchPaneContent").append($("<output/>", { id: "eeResults" }));
    
    $("#eeAbortLink").click(function() {
      setStatus(txt("statusAborted"));
      aborted = true;
      $("eeAbortDiv").hide();
    });
  
    $("#eeSearch").click(function() {
      // Get options from UI
      useSkiplist = $("#eeOptionCB_useSkiplist").attr("checked");
      
      chunkSize = Number($("#eeOptionCB_chunkSize").val());
      if (isNaN(chunkSize) || chunkSize <= 0) {
        setStatus(txt("statusInvalidChunkSize"));
        return;
      }
      
      namespaces = [];
      $("input[id^=eeNamespaceCB_]").each(function() {
        if ($(this).attr("checked")) {
          namespaces.push(Number($(this).attr("id").substring($(this).attr("id").indexOf("_") + 1)));
        }
      });
      
      // Search
      scanFile();
    });
  }
  
  // Perform dump scan
  function scanFile() {
    var reader = new FileReader();
    var file = document.getElementById("eeFile").files[0];
    
    stop = start + chunkSize;
    
    $("#eeResults").text("");
    $("#eeAbortDiv").show();
    setStatus(txt("statusCheckingRules"));
    setProgress("");
    results = 0;
    aborted = false;
    startTime = new Date();
    
    if (! checkRules()) {
      return;
    }
    
    setStatus(txt("statusStartScanning"));

    reader.onloadend = function(e) {
      if (aborted || e.target.readyState != FileReader.DONE || file == null || ! file) {
        return;
      }
      
      var percentage = (Math.min(stop, file.size) / file.size) * 100;
      setProgress(percentage.toFixed(0) + "% (" + Math.min(stop, file.size) + " of " + file.size + " bytes)");
    
      // 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(txt("statusChunkSizeTooSmall"));
        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(txt("statusUnsupportedDump"));
          return;
        }
        
        setStatus(txt("statusScanningDump").format(base));
      }
      
      // Search
      $(text).find("page").each(function() {
        // Check namespace
        if ($.inArray(Number($(this).find("ns").text()), namespaces) == -1) {
          return;
        }
        
        // Check redirect status
        if (ignoreRedirects && $(this).find("redirect").length > 0) {
          return;
        }
        
        searchByRules($(this));
      });
      
      // Next chunk
      if (stop < file.size) {
        start = start + chunkSize;
        stop = stop + chunkSize;
        
        var blob = file.slice(start, stop);
        reader.readAsText(blob);
      } else {
        var msec = ((new Date()) - startTime);
        var sec = ((Math.floor(msec / 1000) < 10) ? "0" : "") + Math.floor(msec / 1000);
        var min = ((Math.floor(msec / 1000 / 60) < 10) ? "0" : "") + Math.floor(msec / 1000 / 60);
      
        setStatus(txt("statusFinished").format(results, min, sec));
        $("#eeAbortDiv").hide();
      }
    };

    if (file == null || ! file) {
      setStatus(txt("statusNoFile"));
      return;
    }
    
    var blob = file.slice(start, stop + 1);
    reader.readAsText(blob);
  }
  
  // Startup
  $(document).ready(function() {
    if (! window.File || ! window.FileReader || ! window.Blob) {
      setStatus(txt("statusUnsupportedBrowser"));
      return;
    }
  
    if (activateScanner) {
      mw.util.addPortletLink("p-navigation", mw.util.wikiGetlink(scannerPage), txt("generalName"), "EagleEye-portlet", txt("generalStartScanner"));
    }
    
    if (typeof rules === "undefined" || rules == null || rules.length == 0) {
      setStatus(txt("statusRulesetMissing"));
      return;
    }
    
    if (mw.config.get("wgPageName") == scannerPage && mw.config.get("wgAction") === "view" && activateScanner) {
      // Scanner view
      mw.util.addCSS("div.eeSection { border: 1px solid black; padding: 8px; }");
    
      loadScannerUI();
    } else if ($.inArray(mw.config.get("wgNamespaceNumber"), namespaces) != -1) {
      if (mw.config.get("wgAction") === "view" && activateMarker) {
        // Article view
        mw.util.addCSS("span.eeMarker { " + markerStyle + " }");

        markView();
      } else if ($.inArray(mw.config.get("wgAction"), [ "edit", "submit" ]) != -1 && activateScript) {
        // Edit view - add button
        $("#wpTextbox1").on("wikiEditor-toolbar-doneInitialSections", function () {
          mw.loader.using("ext.wikiEditor.toolbar", addButton());
        });
      }
    }
  });
})();