跳转到内容

User:SuperGrey/gadgets/voter/main.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// [[User:SuperGrey/gadgets/voter]]
// Release: 4.2.2
// Timestamp: 2026-02-06T03:21:53.073Z
// <nowiki>
"use strict";
(() => {
  var __defProp = Object.defineProperty;
  var __defProps = Object.defineProperties;
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __spreadValues = (a, b) => {
    for (var prop in b || (b = {}))
      if (__hasOwnProp.call(b, prop))
        __defNormalProp(a, prop, b[prop]);
    if (__getOwnPropSymbols)
      for (var prop of __getOwnPropSymbols(b)) {
        if (__propIsEnum.call(b, prop))
          __defNormalProp(a, prop, b[prop]);
      }
    return a;
  };
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));

  // src/state.ts
  var State = class {
    constructor() {
      // 簡繁轉換
      this.convByVar = (langDict) => {
        if (langDict && langDict.hant) {
          return langDict.hant;
        }
        return "繁簡轉換未初始化,且 langDict 無效!";
      };
      // 用戶名
      this.userName = mw.config.get("wgUserName") || "Example";
      // 頁面名稱
      this.pageName = mw.config.get("wgPageName");
      /**
       * 版本號
       */
      this.version = "4.2.2";
      // MediaWiki API 實例
      this._api = null;
      /**
       * 頁面標題
       * @type {{data: number; label: string;}[]}
       */
      this.sectionTitles = [];
      /**
       * 有效投票模板
       * @type {{data: string; label: string;}[]}
       */
      this.validVoteTemplates = [];
      /**
       * 無效投票模板
       * @type {{data: string; label: string;}[]}
       */
      this.invalidVoteTemplates = [];
    }
    async initHanAssist() {
      const requireModule = await mw.loader.using("ext.gadget.HanAssist");
      const hanAssist = requireModule("ext.gadget.HanAssist");
      if (hanAssist && typeof hanAssist.convByVar === "function") {
        this.convByVar = hanAssist.convByVar;
      }
    }
    getApi() {
      if (!this._api) {
        this._api = new mw.Api({
          ajax: {
            headers: {
              "User-Agent": `Voter/${this.version}`
            }
          }
        });
      }
      return this._api;
    }
  };
  var state = new State();
  var state_default = state;

  // src/api.ts
  async function getXToolsInfo(pageName) {
    const safeToLocaleString = (value) => {
      if (typeof value === "number" && !isNaN(value)) {
        return value.toLocaleString();
      }
      return "0";
    };
    try {
      const pageInfo = await $.get("https://xtools.wmcloud.org/api/page/pageinfo/" + mw.config.get("wgServerName") + "/" + pageName.replace(/["?%&+\\]/g, escape));
      const project = pageInfo.project;
      const pageEnc = encodeURIComponent(pageInfo.page);
      const pageUrl = `https://${project}/wiki/${pageInfo.page}`;
      const pageinfoUrl = `https://xtools.wmcloud.org/pageinfo/${project}/${pageEnc}`;
      const permaLinkUrl = `https://${project}/wiki/Special:PermaLink%2F${pageInfo.created_rev_id}`;
      const diffUrl = `https://${project}/wiki/Special:Diff%2F${pageInfo.modified_rev_id}`;
      const pageviewsUrl = `https://pageviews.wmcloud.org/?project=${project}&pages=${pageEnc}&range=latest-${pageInfo.pageviews_offset}`;
      const creatorLink = `https://${project}/wiki/User:${pageInfo.creator}`;
      const creatorContribsUrl = `https://${project}/wiki/Special:Contributions/${pageInfo.creator}`;
      const createdDate = new Date(pageInfo.created_at).toISOString().split("T")[0];
      const revisionsText = safeToLocaleString(pageInfo.revisions);
      const editorsText = safeToLocaleString(pageInfo.editors);
      const watchersText = safeToLocaleString(pageInfo.watchers);
      const pageviewsText = safeToLocaleString(pageInfo.pageviews);
      const days = Math.round(pageInfo.secs_since_last_edit / 86400);
      let creatorText = "";
      if (pageInfo.creator_editcount) {
        creatorText = `<bdi><a href="${creatorLink}" target="_blank">${pageInfo.creator}</a></bdi> (<a href="${creatorContribsUrl}" target="_blank">${safeToLocaleString(pageInfo.creator_editcount)}</a>)`;
      } else {
        creatorText = `<bdi><a href="${creatorContribsUrl}" target="_blank">${pageInfo.creator}</a></bdi>`;
      }
      let pageCreationText = `「<a target="_blank" title="評級: ${pageInfo.assessment.value}" href="${pageinfoUrl}"><img src="${pageInfo.assessment.badge}" style="height:16px !important; vertical-align:-4px; margin-right:3px"/></a><bdi><a target="_blank" href="${pageUrl}">${pageInfo.page}</a></bdi>」由 ${creatorText} 於 <bdi><a target='_blank' href='${permaLinkUrl}'>${createdDate}</a></bdi> 建立,共 ${revisionsText} 個修訂,最後修訂於 <a href="${diffUrl}">${days} 天</a>前。`;
      let pageEditorsText = `共 ${editorsText} 編輯者` + (watchersText !== "0" ? `、${watchersText} 監視者` : "") + `,最近 ${pageInfo.pageviews_offset} 天共 <a target="_blank" href="${pageviewsUrl}">${pageviewsText} 瀏覽數</a>。`;
      return `<span style="line-height:20px">${pageCreationText}${pageEditorsText}<a target="_blank" href="${pageinfoUrl}">檢視完整頁面統計</a>。</span>`.trim();
    } catch (error) {
      console.error("[Voter] Error fetching XTools data:", error);
      return '<span style="color: red; font-weight: bold;">無法獲取 XTools 頁面資訊。</span>';
    }
  }
  async function voteAPI(tracePage, destPage, sectionID, text, summary) {
    var _a, _b;
    const votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === sectionID)) == null ? void 0 : _a.label) || `section ${sectionID}`;
    mw.notify(`正在為「${votedPageName}」投出一票⋯⋯`);
    const res = await state_default.getApi().get({
      action: "query",
      titles: destPage,
      prop: "revisions|info",
      rvslots: "*",
      rvprop: "content",
      rvsection: sectionID,
      indexpageids: 1
    });
    const firstPageId = res.query.pageids[0];
    const page = res.query.pages[firstPageId];
    const firstRevision = (_b = page == null ? void 0 : page.revisions) == null ? void 0 : _b[0];
    const sectionText = firstRevision == null ? void 0 : firstRevision.slots.main["*"];
    if (sectionText === void 0 || sectionText === "") {
      console.log(`[Voter] 無法取得「${votedPageName}」的投票區段內容。區段ID:${sectionID}。API 回傳:`, res);
      mw.notify(`無法取得「${votedPageName}」的投票區段內容,請刷新後重試。`);
      return true;
    }
    if (!textMatchTitleVariants(sectionText, votedPageName)) {
      console.log(`[Voter] 在「${votedPageName}」的投票區段中找不到該條目。區段文本:`, sectionText);
      mw.notify(`在該章節找不到名為「${votedPageName}」的提名,請刷新後重試。`);
      return true;
    }
    let innerHeadings;
    if (tracePage === "Wikipedia:新条目推荐/候选") {
      innerHeadings = sectionText.match(/=====.+?=====/g);
    } else {
      innerHeadings = sectionText.match(/===.+?===/g);
    }
    const targetSection = innerHeadings ? sectionID + 1 : sectionID;
    const editParams = {
      action: "edit",
      title: destPage,
      section: targetSection,
      summary,
      token: mw.user.tokens.get("csrfToken")
    };
    if (innerHeadings) {
      editParams.prependtext = `${text}
`;
    } else {
      editParams.appendtext = `
${text}`;
    }
    await state_default.getApi().post(editParams);
    mw.notify(`「${votedPageName}」已完成投票。`);
    return false;
  }

  // src/build_comment.ts
  function isListLineNode(node) {
    return node.text !== void 0;
  }
  var SPACE_AFTER_INDENTATION_CHARS = true;
  var PARAGRAPH_TEMPLATES = [];
  var FILE_PREFIX_PATTERN = "(?:File|Image)";
  var MASK_PREFIX = "<<VOTER_MASK_";
  var MASK_SUFFIX = "_VOTER>>";
  var MASK_ANY_REGEXP = /<<VOTER_MASK_(\d+)(?:_\w+(?:_\d+)?)?_VOTER>>/g;
  var POPULAR_NOT_INLINE_ELEMENTS = [
    "BLOCKQUOTE",
    "CAPTION",
    "CENTER",
    "DD",
    "DIV",
    "DL",
    "DT",
    "FIGURE",
    "FIGCAPTION",
    "FORM",
    "H1",
    "H2",
    "H3",
    "H4",
    "H5",
    "H6",
    "HR",
    "INPUT",
    "LI",
    "LINK",
    "OL",
    "P",
    "PRE",
    "SECTION",
    "STYLE",
    "TABLE",
    "TBODY",
    "TD",
    "TFOOT",
    "TH",
    "THEAD",
    "TR",
    "UL"
  ];
  var PNIE_PATTERN = `(?:${POPULAR_NOT_INLINE_ELEMENTS.join("|")})`;
  var FILE_PATTERN_END = `\\[\\[${FILE_PREFIX_PATTERN}:.+\\]\\]$`;
  var GALLERY_REGEXP = /^<<VOTER_MASK_\d+_gallery_VOTER>>$/m;
  function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  function generateTagsRegexp(tags) {
    const tagsJoined = tags.join("|");
    return new RegExp(`(<(${tagsJoined})(?: [\\w ]+(?:=[^<>]+?)?| *)>)([^]*?)(</\\2>)`, "ig");
  }
  var TextMasker = class {
    constructor(text, maskedTexts) {
      this.text = text;
      this.maskedTexts = maskedTexts || [];
    }
    /**
     * 使用指定的正則表達式和類型對文本進行遮罩處理,將匹配的文本替換為特殊標記並存儲原始文本。
     * @param {RegExp} regexp 用於匹配要遮罩的文本的正則表達式
     * @param {string} [type] 遮罩類型,用於生成不同的標記
     * @param {boolean} [useGroups=false] 是否使用正則表達式的捕獲組來分割前綴文本和要遮罩的文本
     * @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
     */
    mask(regexp, type, useGroups = false) {
      this.text = this.text.replace(regexp, (s, preText, textToMask) => {
        if (!useGroups) {
          preText = "";
          textToMask = s;
        }
        const masked = textToMask || s;
        const id = this.maskedTexts.push(masked);
        const typeSuffix = type ? `_${type}` : "";
        return `${preText || ""}${MASK_PREFIX}${id}${typeSuffix}${MASK_SUFFIX}`;
      });
      return this;
    }
    /**
     * 根據指定的類型對文本進行解遮罩處理,將特殊標記替換回原始文本。
     * @param {string} text 要解遮罩的文本
     * @param {string} [type] 遮罩類型,用於匹配對應的標記
     * @returns {string} 解遮罩後的文本
     */
    unmaskText(text, type) {
      const regexp = type ? new RegExp(
        `${escapeRegExp(MASK_PREFIX)}(\\d+)(?:_${escapeRegExp(type)}(?:_\\d+)?)?${escapeRegExp(MASK_SUFFIX)}`,
        "g"
      ) : MASK_ANY_REGEXP;
      while (regexp.test(text)) {
        text = text.replace(regexp, (_s, num) => this.maskedTexts[Number(num) - 1]);
      }
      return text;
    }
    /**
     * 對文本進行解遮罩處理,將所有特殊標記替換回原始文本。
     * @param {string} [type] 遮罩類型,用於匹配對應的標記
     * @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
     */
    unmask(type) {
      this.text = this.unmaskText(this.text, type);
      return this;
    }
    /**
     * 遞歸地遮罩文本中的模板,將匹配的模板替換為特殊標記並存儲原始模板文本。
     * @param {(templateCode: string) => string} [handler] 可選的處理函數,用於在遮罩前對模板文本進行修改
     * @param {boolean} [addLengths=false] 是否在標記中添加原始模板文本的長度信息
     * @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
     */
    maskTemplatesRecursively(handler, addLengths = false) {
      let pos = 0;
      const stack = [];
      while (true) {
        let left = this.text.indexOf("{{", pos);
        let right = this.text.indexOf("}}", pos);
        if (left !== -1 && left < right) {
          stack.push(left);
          pos = left + 2;
        } else {
          if (!stack.length) break;
          left = stack.pop();
          if (typeof left === "undefined") {
            if (right === -1) {
              pos += 2;
              continue;
            } else {
              left = 0;
            }
          }
          if (right === -1) {
            right = this.text.length;
          }
          right += 2;
          let template = this.text.substring(left, right);
          if (handler) {
            template = handler(template);
          }
          const lengthOrNot = addLengths ? "_" + template.replace(
            new RegExp(
              `${escapeRegExp(MASK_PREFIX)}\\d+_template_(\\d+)${escapeRegExp(MASK_SUFFIX)}`,
              "g"
            ),
            (_m, n) => new Array(Number(n) + 1).join(" ")
          ).length : "";
          this.text = this.text.substring(0, left) + MASK_PREFIX + this.maskedTexts.push(template) + "_template" + lengthOrNot + MASK_SUFFIX + this.text.slice(right);
          pos = right - template.length;
        }
      }
      return this;
    }
    /**
     * 對文本中的指定標籤進行遮罩處理,將匹配的標籤替換為特殊標記並存儲原始標籤文本。
     * @param {string[]} tags 標籤名稱列表
     * @param {string} type 遮罩類型,用於生成不同的標記
     * @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
     */
    maskTags(tags, type) {
      return this.mask(generateTagsRegexp(tags), type);
    }
    /**
     * 對敏感代碼區塊進行遮罩處理(block/gallery/nowiki/template/table)。
     * @param {(templateCode: string) => string} [templateHandler] 可選的處理函數,用於在遮罩前對模板文本進行修改
     * @returns {this} 返回當前 TextMasker 實例以支持鏈式調用
     */
    maskSensitiveCode(templateHandler) {
      return this.maskTags(["pre", "source", "syntaxhighlight"], "block").maskTags(["gallery", "poem"], "gallery").maskTags(["nowiki"], "inline").maskTemplatesRecursively(templateHandler).mask(/^(:* *)(\{\|[^]*?\n\|\})/gm, "table", true).mask(/^(:* *)(\{\|[^]*\n\|)/gm, "table", true);
    }
  };
  function prependIndentationToLine(indentation, line) {
    return indentation + (indentation && SPACE_AFTER_INDENTATION_CHARS && !/^[:*#;]/.test(line) ? " " : "") + line;
  }
  function linesToLists(lines, isNested = false) {
    const listTags = { ":": "dl", ";": "dl", "*": "ul", "#": "ol" };
    const itemTags = { ":": "dd", ";": "dt", "*": "li", "#": "li" };
    let list = { items: [] };
    for (let i = 0; i <= lines.length; i++) {
      if (i === lines.length) {
        if (list.type) {
          lineToList(lines, i, list, isNested);
        }
      } else {
        const node = lines[i];
        const text = isListLineNode(node) ? node.text : "";
        const firstChar = text[0] || "";
        const listType = listTags[firstChar];
        if (list.type && listType !== list.type) {
          const itemsCount = list.items.length;
          lineToList(lines, i, list, isNested);
          i -= itemsCount - 1;
          list = { items: [] };
        }
        if (listType) {
          list.type = listType;
          list.items.push({
            type: itemTags[firstChar],
            text: text.slice(1)
          });
        }
      }
    }
    return lines;
  }
  function lineToList(lines, i, list, isNested = false) {
    if (isNested) {
      const previousItemIndex = i - list.items.length - 1;
      if (previousItemIndex >= 0) {
        const item = {
          type: lines[previousItemIndex].type,
          items: [lines[previousItemIndex], list]
        };
        lines.splice(previousItemIndex, list.items.length + 1, item);
      } else {
        const item = {
          type: lines[0].type,
          items: [list]
        };
        lines.splice(i - list.items.length, list.items.length, item);
      }
    } else {
      lines.splice(i - list.items.length, list.items.length, list);
    }
    linesToLists(list.items, true);
  }
  function listToTags(lines, isNested = false) {
    let text = "";
    lines.forEach((line, i) => {
      if (!isListLineNode(line)) {
        const itemsText = (line.items || []).map((item) => {
          const itemText = !isListLineNode(item) ? listToTags(item.items || [], true) : String(item.text).trim();
          return item.type ? `<${item.type}>${itemText}</${item.type}>` : itemText;
        }).join("");
        text += `<${line.type}>${itemsText}</${line.type}>`;
      } else {
        text += isNested ? String(line.text).trim() : String(line.text);
      }
      if (i !== lines.length - 1) {
        text += "\n";
      }
    });
    return text;
  }
  function listMarkupToTags(code) {
    const lineObjects = code.split("\n").map((line) => ({ type: "", text: line }));
    return listToTags(linesToLists(lineObjects));
  }
  function findWrapperFlags(text, indentation) {
    if (!indentation) {
      return {
        areThereTagsAroundMultipleLines: false,
        areThereTagsAroundListMarkup: false
      };
    }
    const tagMatches = text.match(generateTagsRegexp(["[a-z]+"])) || [];
    const quoteMatches = text.match(
      /(<(?:blockquote|q)(?: [\w ]+(?:=[^<>]+?)?| *)>)([^]*?)(<\/(?:blockquote|q)>)/ig
    ) || [];
    const matches = tagMatches.concat(quoteMatches);
    return {
      areThereTagsAroundMultipleLines: matches.some((match) => match.indexOf("\n") !== -1),
      areThereTagsAroundListMarkup: matches.some((match) => /\n[:*#;]/.test(match))
    };
  }
  function handleIndentedComment(code, indentation, restLinesIndentation, isWrapped, isInTemplate, flags) {
    if (!indentation) {
      return code;
    }
    code = code.replace(/^ +/gm, "");
    if (/^[:*#;]/m.test(code) && (isWrapped || restLinesIndentation === "#")) {
      if (isInTemplate) {
        code = code.replace(/\|(?:[^|=}]*=)?(?=[:*#;])/, "$&\n");
      }
      code = listMarkupToTags(code);
    }
    code = code.replace(
      new RegExp(`(\\n+)([:*#;]|${escapeRegExp(MASK_PREFIX)}\\d+_table${escapeRegExp(MASK_SUFFIX)}|${FILE_PATTERN_END})`, "gmi"),
      (_s, newlines, nextLine) => (newlines.length > 1 ? "\n\n\n" : "\n") + prependIndentationToLine(restLinesIndentation, nextLine)
    );
    code = code.replace(/(^|[^\n])(<<VOTER_MASK_\d+_gallery_VOTER>>)/g, (_s, before, marker) => `${before}
${marker}`).replace(/<<VOTER_MASK_\d+_gallery_VOTER>>(?=(?:$|[^\n]))/g, (marker) => `${marker}
`);
    if (restLinesIndentation.indexOf("#") !== -1 && /<<VOTER_MASK_\d+_table_VOTER>>/.test(code)) {
      throw new Error("numberedList-table");
    }
    if (restLinesIndentation === "#" && GALLERY_REGEXP.test(code)) {
      throw new Error("numberedList");
    }
    code = code.replace(
      /^((?:[:*#;].+|<<VOTER_MASK_\d+_(?:table|gallery)_VOTER>>))(\n+)(?![:#])/mg,
      (_s, previousLine, newlines) => previousLine + "\n" + prependIndentationToLine(restLinesIndentation, newlines.length > 1 ? "\n\n" : "")
    );
    if (PARAGRAPH_TEMPLATES.length) {
      code = code.replace(/^(.*)\n\n+(?!:)/gm, `$1{{${PARAGRAPH_TEMPLATES[0]}}}
`);
    } else if (flags.areThereTagsAroundMultipleLines) {
      code = code.replace(/^(.*)\n\n+(?!:)/gm, "$1<br> \n");
    } else {
      code = code.replace(
        /^(.*)\n\n+(?!:)/gm,
        (_s, m1) => `${m1}
${prependIndentationToLine(restLinesIndentation, "")}`
      );
    }
    return code;
  }
  function processNewlines(code, indentation, isInTemplate = false) {
    const entireLineRegexp = /^<<VOTER_MASK_\d+_(block|template)(?:_\d+)?_VOTER>> *$/;
    const entireLineFromStartRegexp = /^(=+).*\1[ \t]*$|^----/;
    const fileRegexp = new RegExp(`^${FILE_PATTERN_END}`, "i");
    let currentLineInTemplates = "";
    let nextLineInTemplates = "";
    if (isInTemplate) {
      currentLineInTemplates = "|=";
      nextLineInTemplates = "|\\||}}";
    }
    const paragraphTemplatePattern = PARAGRAPH_TEMPLATES.length ? mw.util.escapeRegExp(`{{${PARAGRAPH_TEMPLATES[0]}}}`) : "(?!)";
    const currentLineEndingRegexp = new RegExp(
      `(?:<${PNIE_PATTERN}(?: [\\w ]+?=[^<>]+?| ?\\/?)>|<\\/${PNIE_PATTERN}>|${escapeRegExp(MASK_PREFIX)}\\d+_block${escapeRegExp(MASK_SUFFIX)}|<br[ \\n]*\\/?>|${paragraphTemplatePattern}${currentLineInTemplates}) *$`,
      "i"
    );
    const nextLineBeginningRegexp = new RegExp(
      `^(?:<\\/${PNIE_PATTERN}>|<${PNIE_PATTERN}${nextLineInTemplates})`,
      "i"
    );
    const newlinesRegexp = indentation ? /^(.+)\n(?![:#])(?=(.*))/gm : new RegExp(
      `^((?![:*#; ]).+)\\n(?![\\n:*#; ]|${escapeRegExp(MASK_PREFIX)}\\d+_table${escapeRegExp(MASK_SUFFIX)})(?=(.*))`,
      "gm"
    );
    return code.replace(newlinesRegexp, (_s, currentLine, nextLine) => {
      const lineBreakOrNot = entireLineRegexp.test(currentLine) || entireLineRegexp.test(nextLine) || !indentation && (entireLineFromStartRegexp.test(currentLine) || entireLineFromStartRegexp.test(nextLine)) || fileRegexp.test(currentLine) || fileRegexp.test(nextLine) || GALLERY_REGEXP.test(currentLine) || GALLERY_REGEXP.test(nextLine) || currentLineEndingRegexp.test(currentLine) || nextLineBeginningRegexp.test(nextLine) ? "" : `<br>${indentation ? " " : ""}`;
      const newlineOrNot = indentation && !GALLERY_REGEXP.test(nextLine) ? "" : "\n";
      return currentLine + lineBreakOrNot + newlineOrNot;
    });
  }
  function processCode(code, indentation, restLinesIndentation, flags, isInTemplate) {
    let result = handleIndentedComment(
      code,
      indentation,
      restLinesIndentation,
      isInTemplate || flags.areThereTagsAroundListMarkup,
      isInTemplate,
      flags
    );
    result = processNewlines(result, indentation, isInTemplate);
    return result;
  }
  function buildWikitext(text, indent) {
    const indentation = indent || "";
    const restLinesIndentation = indentation ? indentation.replace(/\*/g, ":") : "";
    const masker = new TextMasker((text || "").trim());
    masker.maskSensitiveCode((templateCode) => processCode(
      templateCode,
      indentation,
      restLinesIndentation,
      {
        areThereTagsAroundMultipleLines: false,
        areThereTagsAroundListMarkup: false
      },
      true
    ));
    const flags = findWrapperFlags(masker.text, indentation);
    masker.text = processCode(masker.text, indentation, restLinesIndentation, flags, false);
    masker.text += "\n";
    let finalIndentation = indentation;
    if (finalIndentation && /^[*#;\x03]/.test(masker.text)) {
      finalIndentation = restLinesIndentation;
    }
    masker.text = prependIndentationToLine(finalIndentation, masker.text);
    masker.unmask();
    return masker.text;
  }

  // src/dialog.ts
  var mountedApp = null;
  var mountedRoot = null;
  var MOUNT_ID = "voter-dialog-mount";
  function loadCodex() {
    return new Promise((resolve, reject) => {
      mw.loader.using("@wikimedia/codex").then((requireFn) => {
        resolve({
          Vue: requireFn ? requireFn("vue") : null,
          Codex: requireFn ? requireFn("@wikimedia/codex") : null
        });
      }).catch((err) => {
        const reason = err instanceof Error ? err : new Error(
          typeof err === "string" ? err : (() => {
            try {
              return JSON.stringify(err);
            } catch (e) {
              return "Unknown error";
            }
          })()
        );
        reject(reason);
      });
    });
  }
  async function loadCodexAndVue() {
    const loaded = await loadCodex();
    return loaded;
  }
  function ensureMount(id = MOUNT_ID) {
    let mount = document.getElementById(id);
    if (!mount) {
      mount = document.createElement("div");
      mount.id = id;
      document.body.appendChild(mount);
    }
    return mount;
  }
  function createDialogMountIfNeeded() {
    return ensureMount(MOUNT_ID);
  }
  function mountApp(app) {
    createDialogMountIfNeeded();
    mountedApp = app;
    mountedRoot = mountedApp.mount(`#${MOUNT_ID}`);
    return mountedApp;
  }
  function getMountedApp() {
    return mountedApp;
  }
  function removeDialogMount() {
    const mount = document.getElementById(MOUNT_ID);
    if (mount) {
      mount.remove();
    }
    mountedApp = null;
    mountedRoot = null;
  }
  function registerCodexComponents(app, Codex) {
    if (!app || !app.component || !Codex) return;
    try {
      if (Codex.CdxDialog) app.component("cdx-dialog", Codex.CdxDialog);
      if (Codex.CdxButton) app.component("cdx-button", Codex.CdxButton);
      if (Codex.CdxSelect) app.component("cdx-select", Codex.CdxSelect);
      if (Codex.CdxTextInput) app.component("cdx-text-input", Codex.CdxTextInput);
      if (Codex.CdxTextArea) app.component("cdx-text-area", Codex.CdxTextArea);
      if (Codex.CdxCheckbox) app.component("cdx-checkbox", Codex.CdxCheckbox);
      if (Codex.CdxField) app.component("cdx-field", Codex.CdxField);
      if (Codex.CdxMultiselectLookup) app.component("cdx-multiselect-lookup", Codex.CdxMultiselectLookup);
    } catch (e) {
    }
  }

  // src/vote_dialog.ts
  var entryInfoPromiseCache = /* @__PURE__ */ new Map();
  var voteMessageCache = /* @__PURE__ */ new Map();
  var codeMirrorRequirePromise = null;
  function getVoteIndent(useBulleted) {
    if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
      return useBulleted ? "**" : "*:";
    }
    return useBulleted ? "*" : ":";
  }
  function buildFinalVoteWikitext(rawText, indent) {
    let finalVoteText = rawText.trim();
    if (!/--~{3,}/.test(finalVoteText)) {
      finalVoteText += "--~~~~";
    }
    return buildWikitext(finalVoteText, indent);
  }
  function getCachedEntryInfo(entryName) {
    const cached = entryInfoPromiseCache.get(entryName);
    if (cached) return cached;
    const pending = getXToolsInfo(entryName).catch((error) => {
      entryInfoPromiseCache.delete(entryName);
      throw error;
    });
    entryInfoPromiseCache.set(entryName, pending);
    return pending;
  }
  function loadCodeMirrorModules() {
    if (codeMirrorRequirePromise) {
      return codeMirrorRequirePromise;
    }
    codeMirrorRequirePromise = new Promise((resolve, reject) => {
      mw.loader.using(["ext.CodeMirror.v6", "ext.CodeMirror.v6.mode.mediawiki"]).then(
        (requireFn) => resolve(requireFn),
        (error) => {
          const reason = error instanceof Error ? error : new Error(String(error));
          reject(reason);
        }
      );
    });
    return codeMirrorRequirePromise;
  }
  function getEntryNameById(entryId) {
    var _a;
    return ((_a = state_default.sectionTitles.find((x) => x.data === entryId)) == null ? void 0 : _a.label) || "";
  }
  function getCachedVoteMessageById(entryId) {
    const entryName = getEntryNameById(entryId);
    if (!entryName) return void 0;
    return voteMessageCache.get(entryName);
  }
  function setCachedVoteMessageById(entryId, message) {
    const entryName = getEntryNameById(entryId);
    if (!entryName) return;
    voteMessageCache.set(entryName, message);
  }
  function createVoteDialog(sectionID) {
    loadCodexAndVue().then(({ Vue, Codex }) => {
      const app = Vue.createMwApp({
        i18n: {
          dialogTitle: state_default.convByVar({
            hant: `投票助手 (Voter v${state_default.version})`,
            hans: `投票助手 (Voter v${state_default.version})`
          }),
          submitting: state_default.convByVar({ hant: "儲存中…", hans: "保存中…" }),
          submit: state_default.convByVar({ hant: "儲存投票", hans: "保存投票" }),
          cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
          next: state_default.convByVar({ hant: "下一步", hans: "下一步" }),
          previous: state_default.convByVar({ hant: "上一步", hans: "上一步" }),
          // Step 0: Entry Selection
          selectEntriesHeading: state_default.convByVar({ hant: "投票條目", hans: "投票条目" }),
          selectEntriesHint: state_default.convByVar({ hant: "建議在閱讀條目後再投票。", hans: "建议在阅读条目后再投票。" }),
          // Step 1: Per-entry Vote Content
          insertTemplateHeading: state_default.convByVar({ hant: "投票理由", hans: "投票理由" }),
          insertTemplateHint: state_default.convByVar({ hant: "模板按鈕會插入到游標所在位置。", hans: "模板按钮会插入到光标所在位置。" }),
          voteReasonPlaceholder: state_default.convByVar({ hant: "輸入投票內容…", hans: "输入投票内容…" }),
          useBulleted: state_default.convByVar({ hant: "使用 * 縮排", hans: "使用 * 缩进" }),
          // Step 2: Preview
          previewHeading: state_default.convByVar({ hant: "預覽投票內容", hans: "预览投票内容" }),
          previewInfo: state_default.convByVar({ hant: "以下是將要提交的投票內容。", hans: "以下是将要提交的投票内容。" }),
          // Validation
          noEntriesSelected: state_default.convByVar({ hant: "請選擇至少一個投票條目。", hans: "请选择至少一个投票条目。" }),
          noVoteContent: state_default.convByVar({ hant: "請輸入投票內容,或先插入模板。", hans: "请输入投票内容,或先插入模板。" })
        },
        data() {
          var _a;
          const defaultVoteMessage = state_default.validVoteTemplates.length > 0 ? `{{${state_default.validVoteTemplates[0].data}}}。` : "";
          const initialVoteMessage = (_a = getCachedVoteMessageById(sectionID)) != null ? _a : defaultVoteMessage;
          return {
            open: true,
            isSubmitting: false,
            currentStep: 0,
            totalSteps: 3,
            // Step 0: Entry selection
            selectedEntries: [sectionID],
            entryInfoHtml: "",
            entryInfoById: {},
            isLoadingInfo: false,
            // Step 1: Per-entry vote content
            voteMessages: {
              [sectionID]: initialVoteMessage
            },
            codeMirrorByEntryId: {},
            useBulleted: true
          };
        },
        computed: {
          entryOptions() {
            return state_default.sectionTitles.map((item) => ({ value: item.data, label: item.label }));
          },
          validTemplateOptions() {
            return (state_default.validVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
          },
          invalidTemplateOptions() {
            return (state_default.invalidVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
          },
          allTemplateOptions() {
            return [...this.validTemplateOptions, ...this.invalidTemplateOptions];
          },
          selectedEntryItems() {
            return this.selectedEntries.map((id) => {
              const entry = state_default.sectionTitles.find((x) => x.data === id);
              return { id, name: entry ? entry.label : `Section ${id}` };
            });
          },
          previewVoteItems() {
            const indent = getVoteIndent(this.useBulleted);
            return this.selectedEntryItems.map((item) => {
              const message = this.voteMessages[item.id] || "";
              return {
                id: item.id,
                name: item.name,
                text: buildFinalVoteWikitext(message, indent)
              };
            });
          },
          primaryAction() {
            if (this.currentStep < this.totalSteps - 1) {
              return { label: this.$options.i18n.next, actionType: "primary", disabled: false };
            }
            return {
              label: this.isSubmitting ? this.$options.i18n.submitting : this.$options.i18n.submit,
              actionType: "progressive",
              disabled: this.isSubmitting
            };
          },
          defaultAction() {
            if (this.currentStep > 0) {
              return { label: this.$options.i18n.previous };
            }
            return { label: this.$options.i18n.cancel };
          }
        },
        watch: {
          selectedEntries: {
            handler() {
              if (this.currentStep === 1) {
                this.syncVoteMessagesFromTextareas();
              }
              this.syncVoteMessages();
              void this.loadEntryInfo();
              if (this.currentStep === 1) {
                setTimeout(() => {
                  void this.syncCodeMirrorInstances();
                }, 0);
              }
            },
            immediate: true
          },
          currentStep(step) {
            if (step === 1) {
              setTimeout(() => {
                void this.syncCodeMirrorInstances();
              }, 0);
            } else {
              this.destroyAllCodeMirror();
            }
          }
        },
        methods: {
          getStepClass(step) {
            return { "voter-multistep-dialog__stepper__step--active": step <= this.currentStep };
          },
          getDefaultVoteMessage() {
            return this.validTemplateOptions.length > 0 ? `{{${this.validTemplateOptions[0].value}}}。` : "";
          },
          syncVoteMessages() {
            var _a;
            const nextMessages = {};
            for (const id of this.selectedEntries) {
              if (Object.prototype.hasOwnProperty.call(this.voteMessages, id)) {
                nextMessages[id] = this.voteMessages[id];
              } else {
                nextMessages[id] = (_a = getCachedVoteMessageById(id)) != null ? _a : this.getDefaultVoteMessage();
              }
              setCachedVoteMessageById(id, nextMessages[id]);
            }
            this.voteMessages = nextMessages;
          },
          syncVoteMessagesFromTextareas() {
            var _a, _b, _c, _d;
            const nextMessages = __spreadValues({}, this.voteMessages);
            for (const id of this.selectedEntries) {
              const binding = this.codeMirrorByEntryId[id];
              const codeMirrorText = (_d = (_c = (_b = (_a = binding == null ? void 0 : binding.cm) == null ? void 0 : _a.view) == null ? void 0 : _b.state) == null ? void 0 : _c.doc) == null ? void 0 : _d.toString();
              if (typeof codeMirrorText === "string") {
                nextMessages[id] = codeMirrorText;
                continue;
              }
              const textarea = this.getVoteTextarea(id);
              if (textarea) {
                nextMessages[id] = textarea.value;
              }
              setCachedVoteMessageById(id, nextMessages[id]);
            }
            this.voteMessages = nextMessages;
          },
          async loadEntryInfo() {
            if (!this.selectedEntries.length) {
              this.entryInfoHtml = "";
              this.entryInfoById = {};
              return;
            }
            this.isLoadingInfo = true;
            const infoPromises = this.selectedEntries.map(async (id) => {
              var _a;
              const entryName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || "";
              if (!entryName) return { id, html: "" };
              const html = await getCachedEntryInfo(entryName);
              return { id, html };
            });
            const results = await Promise.all(infoPromises);
            const nextInfoById = {};
            for (const result of results) {
              nextInfoById[result.id] = result.html || "";
            }
            this.entryInfoById = nextInfoById;
            this.entryInfoHtml = this.selectedEntries.map((id) => this.entryInfoById[id]).filter(Boolean).map((html) => `<div style="margin-top:0.5em">${html}</div>`).join("");
            this.isLoadingInfo = false;
          },
          validateStep0() {
            if (!this.selectedEntries.length) {
              mw.notify(this.$options.i18n.noEntriesSelected, { type: "error", title: "[Voter]" });
              return false;
            }
            return true;
          },
          validateStep1() {
            this.syncVoteMessagesFromTextareas();
            for (const item of this.selectedEntryItems) {
              if (!(this.voteMessages[item.id] || "").trim()) {
                mw.notify(`${this.$options.i18n.noVoteContent} (${item.name})`, { type: "error", title: "[Voter]" });
                return false;
              }
            }
            return true;
          },
          getVoteTextarea(entryId) {
            const container = document.querySelector(`.voter-entry-vote[data-entry-id="${entryId}"]`);
            return container ? container.querySelector("textarea") : null;
          },
          destroyCodeMirrorForEntry(entryId) {
            const binding = this.codeMirrorByEntryId[entryId];
            if (!binding) return;
            binding.textarea.removeEventListener("input", binding.onInput);
            try {
              if (typeof binding.cm.destroy === "function") {
                binding.cm.destroy();
              }
            } catch (error) {
              console.warn("[Voter] Failed to destroy CodeMirror:", error);
            }
            const nextBindings = __spreadValues({}, this.codeMirrorByEntryId);
            delete nextBindings[entryId];
            this.codeMirrorByEntryId = nextBindings;
          },
          destroyAllCodeMirror() {
            const ids = Object.keys(this.codeMirrorByEntryId).map((id) => Number(id));
            for (const id of ids) {
              this.destroyCodeMirrorForEntry(id);
            }
          },
          async initCodeMirrorForEntry(entryId) {
            if (this.codeMirrorByEntryId[entryId]) return;
            const textarea = this.getVoteTextarea(entryId);
            if (!textarea) return;
            try {
              const requireFn = await loadCodeMirrorModules();
              const CodeMirrorCtor = requireFn("ext.CodeMirror.v6");
              const modeModule = requireFn("ext.CodeMirror.v6.mode.mediawiki");
              const mode = typeof modeModule.mediawiki === "function" ? modeModule.mediawiki() : void 0;
              if (!CodeMirrorCtor || !mode) return;
              const cm = new CodeMirrorCtor(textarea, mode);
              cm.initialize();
              const onInput = () => {
                this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
                  [entryId]: textarea.value
                });
              };
              textarea.addEventListener("input", onInput);
              this.codeMirrorByEntryId = __spreadProps(__spreadValues({}, this.codeMirrorByEntryId), {
                [entryId]: { cm, textarea, onInput }
              });
            } catch (error) {
              console.warn("[Voter] CodeMirror initialization failed, fallback to textarea.", error);
            }
          },
          async syncCodeMirrorInstances() {
            if (this.currentStep !== 1) return;
            const selectedIdSet = new Set(this.selectedEntries);
            for (const key of Object.keys(this.codeMirrorByEntryId)) {
              const id = Number(key);
              if (!selectedIdSet.has(id)) {
                this.destroyCodeMirrorForEntry(id);
              }
            }
            for (const id of this.selectedEntries) {
              await this.initCodeMirrorForEntry(id);
            }
          },
          insertTemplate(template, entryId) {
            var _a, _b, _c, _d, _e, _f, _g;
            const templateText = `{{${template}}}`;
            const binding = this.codeMirrorByEntryId[entryId];
            const view = (_a = binding == null ? void 0 : binding.cm) == null ? void 0 : _a.view;
            const selection = (_c = (_b = view == null ? void 0 : view.state) == null ? void 0 : _b.selection) == null ? void 0 : _c.main;
            if (view && selection && typeof view.dispatch === "function") {
              view.dispatch({
                changes: {
                  from: selection.from,
                  to: selection.to,
                  insert: templateText
                },
                selection: { anchor: selection.from + templateText.length }
              });
              const updated = ((_e = (_d = view.state) == null ? void 0 : _d.doc) == null ? void 0 : _e.toString()) || "";
              this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
                [entryId]: updated
              });
              setCachedVoteMessageById(entryId, updated);
              if (typeof view.focus === "function") {
                view.focus();
              }
              return;
            }
            const current = this.voteMessages[entryId] || "";
            const textArea = this.getVoteTextarea(entryId);
            if (!textArea) {
              this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
                [entryId]: `${current}${templateText}`
              });
              setCachedVoteMessageById(entryId, `${current}${templateText}`);
              return;
            }
            const start = (_f = textArea.selectionStart) != null ? _f : current.length;
            const end = (_g = textArea.selectionEnd) != null ? _g : start;
            this.voteMessages = __spreadProps(__spreadValues({}, this.voteMessages), {
              [entryId]: `${current.slice(0, start)}${templateText}${current.slice(end)}`
            });
            setCachedVoteMessageById(entryId, `${current.slice(0, start)}${templateText}${current.slice(end)}`);
            setTimeout(() => {
              const focusedTextArea = this.getVoteTextarea(entryId);
              if (!focusedTextArea) return;
              const nextCursor = start + templateText.length;
              focusedTextArea.focus();
              focusedTextArea.setSelectionRange(nextCursor, nextCursor);
            }, 0);
          },
          onPrimaryAction() {
            if (this.currentStep === 0 && !this.validateStep0()) {
              return;
            }
            if (this.currentStep === 1 && !this.validateStep1()) {
              return;
            }
            if (this.currentStep < this.totalSteps - 1) {
              this.currentStep++;
              return;
            }
            void this.submitVote();
          },
          onDefaultAction() {
            if (this.currentStep > 0) {
              this.currentStep--;
              return;
            }
            this.closeDialog();
          },
          onUpdateOpen(newValue) {
            if (!newValue) {
              this.closeDialog();
            }
          },
          closeDialog() {
            this.destroyAllCodeMirror();
            this.open = false;
            setTimeout(() => {
              removeDialogMount();
            }, 300);
          },
          async submitVote() {
            this.isSubmitting = true;
            this.syncVoteMessagesFromTextareas();
            try {
              const indent = getVoteIndent(this.useBulleted);
              const builtVoteTexts = this.selectedEntries.reduce((acc, id) => {
                acc[id] = buildFinalVoteWikitext(this.voteMessages[id] || "", indent);
                return acc;
              }, {});
              const hasConflict = await vote(this.selectedEntries, builtVoteTexts, this.voteMessages);
              if (hasConflict) {
                this.isSubmitting = false;
                return;
              }
              mw.notify(state_default.convByVar({ hant: "投票已成功提交。", hans: "投票已成功提交。" }), { tag: "voter" });
              this.isSubmitting = false;
              this.open = false;
              setTimeout(() => {
                removeDialogMount();
              }, 300);
            } catch (error) {
              console.error("[Voter] submitVote failed:", error);
              const msg = state_default.convByVar({ hant: "投票提交失敗,請稍後再試。", hans: "投票提交失败,请稍后再试。" });
              mw.notify(msg, { type: "error", title: "[Voter]" });
              this.isSubmitting = false;
            }
          }
        },
        template: `
                <cdx-dialog
                    v-model:open="open"
                    :title="$options.i18n.dialogTitle"
                    :use-close-button="true"
                    :primary-action="primaryAction"
                    :default-action="defaultAction"
                    @primary="onPrimaryAction"
                    @default="onDefaultAction"
                    @update:open="onUpdateOpen"
                    class="voter-dialog voter-multistep-dialog"
                >
                    <template #header>
                        <div class="voter-multistep-dialog__header-top">
                            <h2>{{ $options.i18n.dialogTitle }}</h2>
                        </div>

                        <div class="voter-multistep-dialog__stepper">
                            <div class="voter-multistep-dialog__stepper__label">{{ (currentStep + 1) + ' / ' + totalSteps }}</div>
                            <div class="voter-multistep-dialog__stepper__steps" aria-hidden="true">
                                <span
                                    v-for="step of [0, 1, 2]"
                                    :key="step"
                                    class="voter-multistep-dialog__stepper__step"
                                    :class="getStepClass(step)"
                                ></span>
                            </div>
                        </div>
                    </template>

                    <div v-if="currentStep === 0" class="voter-form-section">
                        <h3>{{ $options.i18n.selectEntriesHeading }}</h3>
                        <div class="voter-template-hint">{{ $options.i18n.selectEntriesHint }}</div>
                        <div class="voter-checkbox-grid">
                            <cdx-checkbox
                                v-for="option in entryOptions"
                                :key="option.value"
                                v-model="selectedEntries"
                                :input-value="option.value"
                            >
                                {{ option.label }}
                            </cdx-checkbox>
                        </div>

                        <div
                            v-if="entryInfoHtml"
                            class="voter-entry-info"
                            v-html="entryInfoHtml"
                        ></div>
                        <div v-else-if="isLoadingInfo" class="voter-entry-info voter-entry-info--loading">
                            載入中...
                        </div>
                    </div>

                    <div v-else-if="currentStep === 1" class="voter-form-section">
						<h3>{{ $options.i18n.insertTemplateHeading }}</h3>
                        <div class="voter-template-hint">{{ $options.i18n.insertTemplateHint }}</div>

                        <div
                            v-for="item in selectedEntryItems"
                            :key="item.id"
                            class="voter-entry-vote"
                            :data-entry-id="item.id"
                        >
                            <div class="voter-entry-vote__title">{{ item.name }}</div>
                            <div class="voter-template-buttons">
                                <cdx-button
                                    v-for="option in allTemplateOptions"
                                    :key="option.value"
                                    @click="insertTemplate(option.value, item.id)"
                                >
                                    {{ option.label }}
                                </cdx-button>
                            </div>
                            <cdx-text-area
                                v-model="voteMessages[item.id]"
                                :placeholder="$options.i18n.voteReasonPlaceholder"
                                rows="3"
                            ></cdx-text-area>
                            <div
                                v-if="entryInfoById[item.id]"
                                class="voter-entry-info voter-entry-info--inline"
                                v-html="entryInfoById[item.id]"
                            ></div>
                        </div>

                        <div class="voter-form-section" style="padding-bottom: 0;">
                            <cdx-checkbox v-model="useBulleted">
                                {{ $options.i18n.useBulleted }}
                            </cdx-checkbox>
                        </div>
                    </div>

                    <div v-else-if="currentStep === 2" class="voter-preview-section">
                        <h3>{{ $options.i18n.previewHeading }}</h3>
                        <div class="voter-template-hint">{{ $options.i18n.previewInfo }}</div>

                        <div
                            v-for="item in previewVoteItems"
                            :key="item.id"
                            class="voter-preview-item"
                        >
                            <strong>{{ item.name }}</strong>
                            <pre class="voter-preview-code">{{ item.text }}</pre>
                        </div>
                    </div>
                </cdx-dialog>
            `
      });
      registerCodexComponents(app, Codex);
      mountApp(app);
    }).catch((error) => {
      console.error("[Voter] 無法加載 Codex:", error);
      mw.notify(state_default.convByVar({ hant: "無法加載對話框組件。", hans: "无法加载对话框组件。" }), {
        type: "error",
        title: "[Voter]"
      });
    });
  }
  function openVoteDialog(sectionID) {
    const mountedApp2 = getMountedApp();
    if (mountedApp2) removeDialogMount();
    createVoteDialog(sectionID);
  }
  window.openVoteDialog = openVoteDialog;

  // src/dom.ts
  function addVoteButtons() {
    var _a;
    if (document.querySelector("#voter-finished-loading")) {
      return;
    }
    state_default.sectionTitles = [];
    let headingSelector;
    if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
      headingSelector = "div.mw-heading.mw-heading4";
    } else {
      headingSelector = "div.mw-heading.mw-heading2";
    }
    $(headingSelector).each((index, element) => {
      let $element = $(element);
      let anchor;
      if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
        anchor = $element.nextUntil(headingSelector, "ul").find("li .anchor").attr("id");
      } else {
        anchor = $element.find("h2").attr("id");
      }
      if (anchor) {
        let sectionID = getSectionID(index + 1);
        const $voteLink = $("<a>").text(state_default.convByVar({ hant: "投票", hans: "投票" })).css({ "cursor": "pointer", "margin-left": "0.25em" });
        $voteLink.on("click", (e) => {
          e.preventDefault();
          openVoteDialog(sectionID);
        });
        $('<span class="mw-editsection-bracket">|</span> ').insertAfter($element.find("span.mw-editsection > a").first());
        $voteLink.insertAfter($element.find("span.mw-editsection > a").first().next());
        state_default.sectionTitles.push({ data: sectionID, label: anchor.replace(/_/g, " ") });
      }
    });
    console.log(`[Voter] 已識別可投票事項共 ${state_default.sectionTitles.length} 項。`);
    let finishedLoading = document.createElement("div");
    finishedLoading.id = "voter-finished-loading";
    finishedLoading.style.display = "none";
    (_a = document.querySelector("#mw-content-text .mw-parser-output")) == null ? void 0 : _a.appendChild(finishedLoading);
  }
  function getSectionID(childid) {
    try {
      let $heading;
      if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
        $heading = $("div.mw-heading.mw-heading4").eq(childid - 1);
      } else {
        $heading = $("div.mw-heading.mw-heading2").eq(childid - 1);
      }
      let $editlink = $heading.find("span.mw-editsection > a");
      let href = $editlink.attr("href");
      if (!href) throw new Error("No href found");
      let match = href.match(/section=(\\d+)/);
      if (match) return +match[1];
      let parts = href.split("&");
      for (let part of parts) {
        if (part.startsWith("section=")) return +part.split("=")[1].replace(/^T-/, "");
      }
    } catch (e) {
      console.log(`[Voter] Failed to get section ID for child ${childid}`);
      throw e;
    }
    return 0;
  }
  function titleVariants(title) {
    let us = title.replace(/ /g, "_");
    let sp = title.replace(/_/g, " ");
    return [title, us, sp, us.charAt(0).toUpperCase() + us.slice(1), sp.charAt(0).toUpperCase() + sp.slice(1)];
  }
  function textMatchTitleVariants(text, title) {
    return titleVariants(title).some((variant) => text.includes(variant));
  }
  function refreshPage(entryName) {
    location.href = mw.util.getUrl(state_default.pageName + "#" + entryName);
    location.reload();
  }
  async function vote(voteIDs, builtVoteTexts, rawVoteTexts) {
    var _a;
    for (const id of voteIDs) {
      const rawVoteText = (rawVoteTexts[id] || "").trim();
      const summaryVoteText = rawVoteText.length > 100 ? `${rawVoteText.slice(0, 100)}...` : rawVoteText;
      let votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || `section ${id}`;
      let destPage = state_default.pageName;
      if (state_default.pageName === "Wikipedia:優良條目評選") {
        destPage += "/提名區";
      } else if (/^Wikipedia:(典范条目评选|特色列表评选)$/i.test(state_default.pageName)) {
        destPage += "/提名区";
      }
      const text = builtVoteTexts[id] || "";
      let summary = `/* ${votedPageName} */ `;
      summary += summaryVoteText || state_default.convByVar({ hant: "投票", hans: "投票" });
      summary += " ([[User:SuperGrey/gadgets/voter|Voter]])";
      if (await voteAPI(state_default.pageName, destPage, id, text, summary)) return true;
    }
    setTimeout(() => {
      var _a2;
      return refreshPage((_a2 = state_default.sectionTitles.find((x) => x.data === voteIDs[0])) == null ? void 0 : _a2.label);
    }, 1e3);
    return false;
  }

  // src/styles.css
  var styles_default = "/* Voter Gadget Styles */\r\n\r\n/* Multi-step Dialog */\r\n.voter-dialog {\r\n    max-width: 600px;\r\n}\r\n\r\n.voter-multistep-dialog__header-top {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: space-between;\r\n    padding: 16px 24px 0;\r\n}\r\n\r\n.voter-multistep-dialog__header-top h2 {\r\n    margin: 0;\r\n    font-size: 18px;\r\n    font-weight: 600;\r\n}\r\n\r\n.voter-multistep-dialog__stepper {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 12px;\r\n    padding: 12px 24px;\r\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\r\n}\r\n\r\n.voter-multistep-dialog__stepper__label {\r\n    font-size: 13px;\r\n    color: #54595d;\r\n    min-width: 36px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__steps {\r\n    display: flex;\r\n    gap: 6px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step {\r\n    display: block;\r\n    width: 12px;\r\n    height: 12px;\r\n    border-radius: 999px;\r\n    background-color: #c8ccd1;\r\n    transition: background-color 0.2s ease;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step--active {\r\n    background-color: #36c;\r\n}\r\n\r\n.cdx-dialog__body h3 {\r\n    margin: 0;\r\n    font-size: 16px;\r\n    font-weight: 600;\r\n}\r\n\r\n/* Form Sections */\r\n.voter-form-section {\r\n    padding: 16px 0;\r\n}\r\n\r\n.voter-form-label {\r\n    display: block;\r\n    font-weight: 600;\r\n    margin-bottom: 8px;\r\n    font-size: 14px;\r\n}\r\n\r\n.voter-checkbox-grid {\r\n    display: grid;\r\n    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\r\n    gap: 0 8px;\r\n}\r\n\r\n.voter-template-hint {\r\n    margin-bottom: 8px;\r\n    font-size: 13px;\r\n    color: #54595d;\r\n}\r\n\r\n.voter-template-buttons {\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    gap: 5px;\r\n    margin-bottom: 5px;\r\n}\r\n\r\n.voter-template-buttons .cdx-button {\r\n    font-size: 13px;\r\n    padding: 2px 5px;\r\n    min-height: inherit;\r\n}\r\n\r\n.voter-entry-vote {\r\n    /* border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    padding: 12px; */\r\n    margin-top: 12px;\r\n}\r\n\r\n.voter-entry-vote__title {\r\n    font-size: 14px;\r\n    font-weight: 600;\r\n    margin-bottom: 8px;\r\n}\r\n\r\n/* Entry Info */\r\n.voter-entry-info {\r\n    margin-top: 12px;\r\n    padding: 12px;\r\n    background-color: #f8f9fa;\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    font-size: 13px;\r\n    line-height: 1.5;\r\n}\r\n\r\n.voter-entry-info--loading {\r\n    color: #72777d;\r\n    font-style: italic;\r\n}\r\n\r\n.voter-entry-info--inline {\r\n    margin-top: 8px;\r\n    margin-bottom: 0;\r\n}\r\n\r\n/* Preview Section */\r\n.voter-preview-item {\r\n    margin-bottom: 16px;\r\n}\r\n\r\n.voter-preview-item strong {\r\n    display: block;\r\n    margin-bottom: 8px;\r\n    font-size: 14px;\r\n}\r\n\r\n.voter-preview-item ul {\r\n    margin: 0;\r\n    padding-left: 24px;\r\n}\r\n\r\n.voter-preview-item li {\r\n    font-size: 14px;\r\n    line-height: 1.6;\r\n}\r\n\r\n.voter-preview-code {\r\n    padding: 12px;\r\n    background-color: #f8f9fa;\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    font-family: monospace;\r\n    font-size: 13px;\r\n    white-space: pre-wrap;\r\n    word-break: break-word;\r\n    margin: 0;\r\n}\r\n\r\n/* Dialog Footer - override Codex defaults */\r\n.voter-dialog footer {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: flex-end;\r\n    gap: 12px;\r\n    border-top: 1px solid rgba(0, 0, 0, 0.06);\r\n    padding: 12px 24px;\r\n}\r\n";

  // src/main.ts
  function injectStyles(css) {
    if (!css) return;
    try {
      const styleEl = document.createElement("style");
      styleEl.appendChild(document.createTextNode(css));
      document.head.appendChild(styleEl);
    } catch (e) {
      const div = document.createElement("div");
      div.innerHTML = `<style>${css}</style>`;
      const styleEl = div.firstElementChild;
      if (styleEl) {
        document.head.appendChild(styleEl);
      }
    }
  }
  function validatePage(pageName) {
    const validPages = [
      {
        name: "Wikipedia:新条目推荐/候选",
        templates: [
          { data: "支持", label: "支持" },
          { data: "反對", label: "反對" },
          { data: "不合要求", label: "不合要求" },
          { data: "問題不當", label: "問題不當" }
        ]
      },
      {
        name: "Wikipedia:優良條目評選",
        templates: [
          { data: "yesGA", label: "符合優良條目標準" },
          { data: "noGA", label: "不符合優良條目標準" }
        ]
      },
      {
        name: "Wikipedia:典范条目评选",
        templates: [
          { data: "yesFA", label: "符合典範條目標準" },
          { data: "noFA", label: "不符合典範條目標準" }
        ]
      },
      {
        name: "Wikipedia:特色列表评选",
        templates: [
          { data: "yesFL", label: "符合特色列表標準" },
          { data: "noFL", label: "不符合特色列表標準" }
        ]
      }
    ];
    for (const page of validPages) {
      if (pageName === page.name || new RegExp(`^${page.name}/`, "i").test(pageName)) {
        state_default.validVoteTemplates = page.templates;
        state_default.invalidVoteTemplates = [
          "中立",
          "意見",
          "建議",
          "疑問",
          "同上",
          "提醒"
        ].map((template) => ({
          data: template,
          label: template
        }));
        return true;
      }
    }
    return false;
  }
  async function init() {
    if (typeof document !== "undefined") {
      injectStyles(styles_default);
    }
    if (!validatePage(state_default.pageName)) {
      console.log("[Voter] 不是目標頁面,小工具終止。");
      return;
    }
    await state_default.initHanAssist();
    console.log(`[Voter] 已載入,當前頁面為 ${state_default.pageName}。`);
    mw.hook("wikipage.content").add(() => {
      setTimeout(() => addVoteButtons(), 200);
    });
  }
  void init();
})();
// </nowiki>