Jump to content

User:DVRTed/lintHelper.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(async () => {
  if (mw.config.get("wgNamespaceNumber") !== 0) return;

  async function check_lint_errors() {
    const api = new mw.Api();
    const $indicator = $("<div>")
      .addClass("mw-indicator")
      .attr("id", "lint-error-indicator")
      .html('<span style="color: #666;">Checking lint...</span>');

    $(".mw-indicators").append($indicator);

    const data = await api.get({
      action: "query",
      list: "linterrors",
      lnttitle: mw.config.get("wgPageName"),
      format: "json",
      formatversion: 2,
    });

    const errors = data.query?.linterrors;
    if (!errors) {
      $indicator.html(
        `<span style="color: green; font-weight: bold;">No lint errors</span>`
      );
      return;
    }

    const error_text = `Found ${errors.length} lint error${
      errors.length > 1 ? "s" : ""
    }`;
    $indicator
      .html(
        `<span style="color: red; font-weight: bold; cursor: pointer;" 
                    id="open_linterror_dialog" title="Click to see detailed lint errors">${error_text}</span>`
      )
      .on("click", () => show_modal(errors));
  }

  function show_modal(errors) {
    $("#lint-modal").remove();

    const $overlay = $("<div>").attr("id", "lint-modal").css(STYLES.overlay);
    const $modal = $("<div>").css(STYLES.modal);
    const $close_btn = $("<button>")
      .text("Close")
      .addClass("mw-ui-button mw-ui-quiet")
      .css(STYLES.close_btn)
      .on("click", () => $overlay.remove());
    const $title = $("<h2>")
      .text(`Lint Errors (${errors.length})`)
      .css(STYLES.title);
    const $error_list = $("<div>");

    errors.forEach((error, i) => {
      const $item = create_error_item(error, i);
      $error_list.append($item);
    });

    $modal.append($close_btn, $title, $error_list);
    $overlay.append($modal);
    $("body").append($overlay);

    // close btn and ESC key
    $overlay.on("click", (e) => {
      if (e.target === e.currentTarget) $overlay.remove();
    });
    $(document).on("keydown.lintModal", (e) => {
      if (e.key === "Escape") {
        $overlay.remove();
        $(document).off("keydown.lintModal");
      }
    });
  }

  function create_error_item(error, index) {
    const $item = $("<div>").css(STYLES.error_item);
    const $chevron = $("<span>").css(STYLES.chevron).html("▶");
    const $header = $("<div>").css(STYLES.header);
    const $title_section = $("<div>")
      .css(STYLES.title_section)
      .append($chevron, $("<strong>").text(error.category));
    const $hint = $("<span>").css(STYLES.hint).text("Click to expand");
    const $details = $("<div>").css(STYLES.details);
    const $expanded = $("<div>").css(STYLES.expanded);

    $item.hover(
      () =>
        $item.css({
          backgroundColor: "#f0f8ff",
          borderColor: "#0645ad",
          boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
        }),
      () =>
        $item.css({
          backgroundColor: "#fafafa",
          borderColor: "#ddd",
          boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
        })
    );

    if (error.templateInfo?.name)
      $details.append(
        `Through the template: <b>${error.templateInfo.name}</b>`
      );
    if (error.params?.name)
      $details.append(`<div>Element: ${error.params.name}</div>`);

    const $context_area = $("<div>")
      .css(STYLES.context)
      .text("Loading context...");
    $expanded.append(
      $("<div>")
        .css({ fontWeight: "bold", marginBottom: "5px" })
        .text("Context:"),
      $context_area
    );

    $header.append($title_section, $hint);
    $item.append($header, $details, $expanded);

    let is_expanded = false;
    $item.on("click", async (e) => {
      if ($(e.target).is("button")) return;

      if (!is_expanded) {
        $expanded.slideDown(200);
        $chevron.css("transform", "rotate(90deg)");
        $hint.text("Click to collapse");
        is_expanded = true;
        await load_context(error, $context_area);
      } else {
        $expanded.slideUp(200);
        $chevron.css("transform", "rotate(0deg)");
        $hint.text("Click to expand");
        is_expanded = false;
      }
    });

    return $item;
  }

  async function load_context(error, $context_area) {
    if (!error.location) return;

    try {
      const api = new mw.Api();
      const data = await api.get({
        action: "query",
        titles: mw.config.get("wgPageName"),
        prop: "revisions",
        rvprop: "content",
        format: "json",
      });

      const page_id = Object.keys(data.query.pages)[0];
      const wikitext = data.query.pages[page_id].revisions[0]["*"];
      const [start, end] = error.location;

      const context_start = Math.max(0, start - 100);
      const context_end = Math.min(wikitext.length, end + 100);

      const before = wikitext.slice(context_start, start);
      const error_text = wikitext.slice(start, end);
      const after = wikitext.slice(end, context_end);

      const $context = $("<span>")
        .append(document.createTextNode(before))
        .append(
          $("<span>")
            .css({
              backgroundColor: "#ffcccc",
              color: "#d33",
              fontWeight: "bold",
            })
            .text(error_text)
        )
        .append(document.createTextNode(after));

      $context_area.empty().append($context);
    } catch (err) {
      $context_area.text("Error loading context: " + err.message);
    }
  }

  const STYLES = {
    indicator: { color: "#666" },
    success: { color: "green", fontWeight: "bold" },
    error: { color: "red", fontWeight: "bold", cursor: "pointer" },
    overlay: {
      position: "fixed",
      top: "0",
      left: "0",
      width: "100%",
      height: "100%",
      backgroundColor: "rgba(0,0,0,0.5)",
      zIndex: "9999",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
    },
    modal: {
      background: "white",
      borderRadius: "8px",
      boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
      maxWidth: "800px",
      maxHeight: "80vh",
      width: "90%",
      padding: "20px",
      overflowY: "auto",
      position: "relative",
    },
    close_btn: { position: "absolute", top: "15px", right: "20px" },
    title: {
      marginTop: "0",
      marginBottom: "20px",
      color: "#333",
      borderBottom: "2px solid #eee",
      paddingBottom: "10px",
    },
    error_item: {
      border: "1px solid #ddd",
      borderRadius: "4px",
      padding: "15px",
      marginBottom: "10px",
      backgroundColor: "#fafafa",
      cursor: "pointer",
      transition: "all 0.2s ease",
      boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
    },
    chevron: {
      display: "inline-block",
      marginRight: "8px",
      transition: "transform 0.2s ease",
      fontSize: "12px",
      color: "#666",
      fontWeight: "bold",
    },
    header: {
      color: "#d33",
      marginBottom: "8px",
      fontSize: "16px",
      display: "flex",
      alignItems: "center",
      justifyContent: "space-between",
    },
    title_section: { display: "flex", alignItems: "center" },
    hint: {
      color: "#666",
      fontSize: "12px",
      fontStyle: "italic",
      fontWeight: "normal",
    },
    details: {
      fontFamily: "monospace",
      fontSize: "14px",
      color: "#666",
      marginLeft: "20px",
    },
    expanded: {
      marginTop: "15px",
      paddingTop: "15px",
      borderTop: "1px solid #ddd",
      display: "none",
      marginLeft: "20px",
    },
    context: {
      background: "#f8f9fa",
      border: "1px solid #eee",
      borderRadius: "4px",
      padding: "10px",
      fontFamily: "monospace",
      fontSize: "13px",
      whiteSpace: "pre-wrap",
      wordBreak: "break-all",
      maxHeight: "200px",
      overflowY: "auto",
    },
  };

  await mw.loader.using(["mediawiki.api"]);
  await check_lint_errors();
})();