MediaWiki:Gadget-tidy-on-keydown.js

Definition från Wiktionary, den fria ordlistan.
Hoppa till navigering Hoppa till sök

OBS: Efter du har publicerat sidan kan du behöva tömma din webbläsares cache för att se ändringarna.

  • Firefox / Safari: Håll ned Skift och klicka på Uppdatera sidan eller tryck Ctrl-F5 eller Ctrl-R (⌘-R på Mac)
  • Google Chrome: Tryck Ctrl-Skift-R (⌘-Skift-R på Mac)
  • Internet Explorer / Edge: Håll ned Ctrl och klicka på Uppdatera eller tryck Ctrl-F5
  • Opera: Tryck Ctrl-F5.
// @ts-check

module.exports = processOnKeydown;

/**
 * @typedef {import("./tidy-data").Data} Data
 * @typedef {import("./tidy-data").Template} Template
 * @typedef {import("./tidy-data").LangCode} LangCode
 * @typedef {{
 *  text: string;
 *  cursor: number;
 * }} TextboxInfo
 * @typedef {{
 *  prevBreak: number;
 *  nextBreak: number;
 *  start: string;
 *  end: string;
 * }} LineInfo
 * @typedef {[before: string, after: string]} InsertedText
 * @typedef {(tb: TextboxInfo, line: LineInfo, data: Data) => InsertedText | undefined} Handler
 **/

/** @type {Record<string, Handler | undefined>} */
var handlers = {
  "|": handlePipe,
  "=": handleEq,
  ">": handleGt,
};

/**
 * @param {string} key
 * @param {() => TextboxInfo | undefined} getTextbox
 * @param {Data} data
 * @returns {InsertedText | undefined}
 * If the current key should be `preventDefault`ed, return the text to be
 * inserted, otherwise return `undefined`.
 */
function processOnKeydown(key, getTextbox, data) {
  var handler = handlers[key];
  if (!handler) return;

  var tb = getTextbox();
  if (!tb) return;

  var text = tb.text;
  var cursor = tb.cursor;

  // Get line info.
  var prevBreak = text.lastIndexOf("\n", cursor - 1);
  prevBreak =
    prevBreak === -1
      ? // Start of text.
        0
      : // Skip past the `\n` char.
        prevBreak + 1;

  var nextBreak = text.indexOf("\n", cursor);
  if (nextBreak === -1) {
    nextBreak = text.length;
  }

  /** @type {LineInfo} */
  var line = {
    prevBreak: prevBreak,
    nextBreak: nextBreak,
    start: text.substring(prevBreak, cursor),
    end: text.substring(cursor, nextBreak),
  };

  return handler(tb, line, data);
}

/**
 * @template {string} T
 * @param {string} str
 * @param {T[]} search
 */
function count(str, search) {
  /** @type {Record<T, number>} */
  var r = /** @type {*} */ ({});

  search.forEach(function (s) {
    r[s] = 0;
    for (var i = 0; ; i++, r[s]++) {
      i = str.indexOf(s, i);
      if (i === -1) {
        break;
      }
    }
  });

  return r;
}

/**
 * Handle `|` as part of templates that need a language code.
 * @type {Handler}
 */
function handlePipe(tb, line, data) {
  var templateStart = line.start.lastIndexOf("{{");

  var before = count(line.start, ["{{", "}}"]);
  var after = count(line.end, ["{{", "}}"]);
  var canInsert =
    // At least one template on the start of the line must not have been
    // completed.
    before["{{"] > before["}}"] &&
    // Taken as a whole line, at least one template must not have been
    // completed.
    before["{{"] + after["{{"] > before["}}"] + after["}}"];

  if (!canInsert) {
    return;
  }

  var templateName = /** @type {Template} */ (
    line.start.substring(templateStart + 2)
  );

  /** @type {Map<string, LangCode>} */
  var langs;
  /** @type {LangCode | undefined | null} */
  var langCode;

  if (templateName === "ö" || templateName === "ö+") {
    langs = data.langCodesByLcName;
    var match = /^\*\*?([^:]+):/m.exec(line.start);

    if (!match) return;

    langCode = langs.get(match[1]);
    if (!langCode) {
      if (!line.start.startsWith("**")) return;

      match = findPrev(tb.text, /^\*([^:*]+):.*?\n/gm, tb.cursor);
      langCode = match && langs.get(match[1]);
      if (!match || !langCode) return;

      // Every line between the matching language and the cursor must be a
      // line starting with `**`. Otherwise, there is something weird going
      // on, which we shouldn't support.
      var onlySubLangsBetween = tb.text
        .slice(match.index + match[0].length, tb.cursor)
        .split("\n")
        .every(function (x) {
          return x.startsWith("**");
        });

      if (!onlySubLangsBetween) return;
    }

    return ["|" + langCode + "|", "}}"];
  } else {
    langs = data.langCodesByUcfirstName;
    var paramInfo = data.langCodeTemplates.get(templateName);
    if (!paramInfo || paramInfo === "språk") return;

    var langHeader = findPrev(tb.text, /^==([^=]+)==$/gm, tb.cursor);
    if (!langHeader) return;
    var langName = langHeader[1];

    langCode = langs.get(langName);
    if (!langCode) return;

    // Special cases for {{uttal}}.
    if (templateName === "uttal") {
      // Any language code is allowed for {{uttal}} under ====Tvärspråkligt====.
      if (langCode === "--") return;

      // 2+ parameters are required for {{uttal}}, though exactly 1 numeric.
      paramInfo = [2, 2];
    }

    var min = paramInfo[0];
    var max = paramInfo[1];
    return [
      "|" + langCode + (max === 1 ? "}}" : min > 1 ? "|" : ""),
      max === 1 ? "" : "}}",
    ];
  }
}

/**
 * Handle `=` at the end of `==Källor==`.
 * @type {Handler}
 */
function handleEq(tb, line) {
  if (
    line.start === "==Källor=" &&
    line.end === "" &&
    !tb.text.includes("<references")
  ) {
    return ["=\n<references/>", ""];
  }
}

/**
 * Handle `>` at the end of `<ref>`.
 * @type {Handler}
 */
function handleGt(tb, line) {
  if (/<ref(?: [^<>/]*|)$/.test(line.start)) {
    // We're about to complete a start tag (not self-closing nor end).

    // Find the next ref tag (start, self-closing or end).
    var re = /<(\/?)ref[ />]/;
    re.lastIndex = tb.cursor;
    var next = re.exec(tb.text);
    if (next && next[1]) {
      // If the end tag `</ref>` is next, do nothing.
      return;
    }

    // After the cursor: start tag `<ref>` or self-closing `<ref/>`. We can
    // therefor insert the end tag.
    return [">", "</ref>"];
  }
}

/**
 * @param {string} str
 * @param {RegExp} regex
 * @param {number} index
 */
function findPrev(str, regex, index) {
  /** @type {RegExpExecArray | null} */
  var prev = null;

  for (;;) {
    var match = regex.exec(str);
    if (!match || index <= match.index) {
      break;
    }

    prev = match;
  }

  return prev;
}