跳转到内容

User:SuperGrey/sandbox1.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/VGTNTool]]
// <nowiki>
(() => {
  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);

  // src/api.ts
  var _api = null;
  function getApi() {
    if (!_api) {
      _api = new mw.Api({ "User-Agent": "VGTNTool/1.0.2" });
    }
    return _api;
  }
  async function fetchModuleData() {
    const res = await getApi().get({
      action: "expandtemplates",
      text: "{{#invoke:Vgtn/data|toJSON}}",
      prop: "wikitext",
      format: "json"
    });
    try {
      return JSON.parse(res.expandtemplates.wikitext);
    } catch (e) {
      console.error("[VGTNTool] 解析 JSON 時出錯:", e);
      alert("無法載入資料,請稍後再試。");
      console.log("[VGTNTool]", res.expandtemplates.wikitext);
      throw e;
    }
  }
  async function fetchModuleText() {
    const res = await getApi().get({
      action: "query",
      titles: "Module:Vgtn/data",
      curtimestamp: 1,
      prop: "revisions",
      indexpageids: 1,
      rvprop: ["timestamp", "content"],
      rvslots: "main"
    });
    const rev = res.query.pages[res.query.pageids[0]].revisions[0];
    return {
      start: rev.curtimestamp,
      base: rev.timestamp,
      fulltext: rev.slots.main["*"]
    };
  }
  async function fetchDiffHtml(oldText, newText) {
    const res = await getApi().postWithToken("csrf", {
      action: "compare",
      fromslots: "main",
      "fromtext-main": oldText,
      fromtitle: "Module:Vgtn/data",
      frompst: "true",
      toslots: "main",
      "totext-main": newText,
      totitle: "Module:Vgtn/data",
      topst: "true"
    });
    return res.compare["*"] ? $("<table>").addClass("diff").append($("<colgroup>").append($("<col>").addClass("diff-marker"), $("<col>").addClass("diff-content"), $("<col>").addClass("diff-marker"), $("<col>").addClass("diff-content"))).append(res.compare["*"]) : null;
  }
  async function saveModuleText(diffText, startTimestamp, baseTimestamp) {
    const res = await getApi().postWithToken("csrf", {
      action: "edit",
      title: "Module:Vgtn/data",
      text: diffText,
      summary: "[[User:SuperGrey/gadgets/VGTNTool|編輯資料]]",
      starttimestamp: startTimestamp,
      basetimestamp: baseTimestamp
    });
    if (res.edit && res.edit.result === "Success") {
      console.log("[VGTNTool] 資料已成功儲存。");
      mw.notify("資料已成功儲存。", { type: "success", autoHide: true, autoHideSeconds: 3 });
      refreshPage();
      return false;
    } else if (res.error && res.error.code === "editconflict") {
      console.error("[VGTNTool] 儲存資料時發生編輯衝突:", res);
      mw.notify("儲存資料時發生編輯衝突。請稍後再試。", { type: "error", autoHide: true, autoHideSeconds: 3 });
      return true;
    } else {
      console.error("[VGTNTool] 儲存資料時出錯:", res, "擬儲存的資料:", diffText);
      mw.notify("儲存資料時出錯。請稍後再試。", { type: "error", autoHide: true, autoHideSeconds: 3 });
      return true;
    }
  }
  function refreshPage() {
    setTimeout(function() {
      window.location.reload();
    }, 2e3);
  }

  // src/state.ts
  var State = class {
    constructor() {
      /**
       * 使用者名稱,從MediaWiki配置中獲取。
       * @type {string}
       * @constant
       */
      __publicField(this, "userName", mw.config.get("wgUserName"));
      /**
       * 頁面名稱,從MediaWiki配置中獲取。
       * @type {string}
       * @constant
       */
      __publicField(this, "pageName", mw.config.get("wgPageName"));
      // 簡繁轉換
      __publicField(this, "convByVar", function(langDict) {
        if (langDict && langDict.hant) {
          return langDict.hant;
        }
        return "繁簡轉換未初始化,且 langDict 無效!";
      });
    }
    async initHanAssist() {
      let require2 = await mw.loader.using("ext.gadget.HanAssist");
      const { convByVar } = require2("ext.gadget.HanAssist");
      if (typeof convByVar === "function") {
        this.convByVar = convByVar;
      }
    }
  };
  var state = new State();
  var state_default = state;

  // src/dialog.ts
  var _mountedApp = null;
  function loadCodexAndVue() {
    return mw.loader.using("@wikimedia/codex").then((require2) => ({
      Vue: require2("vue"),
      Codex: require2("@wikimedia/codex")
    }));
  }
  function createDialogMountIfNeeded() {
    if (!document.getElementById("review-tool-dialog-mount")) {
      const mountPoint = document.createElement("div");
      mountPoint.id = "review-tool-dialog-mount";
      document.body.appendChild(mountPoint);
    }
    return document.getElementById("review-tool-dialog-mount");
  }
  function mountApp(app) {
    createDialogMountIfNeeded();
    _mountedApp = app.mount("#review-tool-dialog-mount");
    return _mountedApp;
  }
  function getMountedApp() {
    return _mountedApp;
  }
  function removeDialogMount() {
    const mountPoint = document.getElementById("review-tool-dialog-mount");
    if (mountPoint) mountPoint.remove();
    _mountedApp = null;
  }
  function registerCodexComponents(app, Codex) {
    if (!app || !Codex) return;
    try {
      app.component("cdx-dialog", Codex.CdxDialog).component("cdx-text-input", Codex.CdxTextInput).component("cdx-text-area", Codex.CdxTextArea).component("cdx-checkbox", Codex.CdxCheckbox).component("cdx-select", Codex.CdxSelect).component("cdx-button", Codex.CdxButton).component("cdx-button-group", Codex.CdxButtonGroup);
    } catch (e) {
    }
  }

  // src/diff_dialog.ts
  async function showDiffDialog(diffHtml, diffText, startTimestamp, baseTimestamp) {
    if (getMountedApp()) removeDialogMount();
    try {
      const { Vue, Codex } = await loadCodexAndVue();
      return await new Promise((resolve) => {
        let resolved = false;
        const finalize = (result_4) => {
          if (resolved) return;
          resolved = true;
          resolve(result_4);
        };
        const htmlString = diffHtml ? diffHtml.prop("outerHTML") || diffHtml.html() || "" : "";
        const app = Vue.createMwApp({
          i18n: {
            title: state_default.convByVar({ hant: "檢視差異", hans: "查看差异" }),
            save: state_default.convByVar({ hant: "儲存", hans: "保存" }),
            cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
            noDiff: state_default.convByVar({ hant: "無差異。", hans: "无差异。" })
          },
          data() {
            return {
              open: true,
              diffHtml: htmlString || "",
              diffText: diffText || "",
              startTimestamp: startTimestamp || "",
              baseTimestamp: baseTimestamp || "",
              saving: false
            };
          },
          computed: {
            hasDiff() {
              return Boolean(this.diffHtml && this.diffHtml !== "");
            },
            primaryAction() {
              return {
                label: this.$options.i18n.save,
                actionType: "progressive",
                disabled: !this.hasDiff || this.saving
              };
            },
            defaultAction() {
              return {
                label: this.$options.i18n.cancel
              };
            }
          },
          methods: {
            async onPrimaryAction() {
              if (this.saving) return;
              this.saving = true;
              try {
                const res = await saveModuleText(this.diffText, this.startTimestamp, this.baseTimestamp);
                this.saving = false;
                if (res === false) {
                  finalize({ action: "save" });
                  this.closeDialog();
                } else {
                  mw && mw.notify && mw.notify(state_default.convByVar({ hant: "儲存時發生錯誤,請稍後再試。", hans: "保存时发生错误,请稍后再试。" }), { type: "error" });
                }
              } catch (err) {
                this.saving = false;
                console.error("[VGTNTool] saveModuleText failed", err);
                mw && mw.notify && mw.notify(state_default.convByVar({ hant: "儲存時發生錯誤,請稍後再試。", hans: "保存时发生错误,请稍后再试。" }), { type: "error" });
              }
            },
            onCancelAction() {
              finalize({ action: "cancel" });
              this.closeDialog();
            },
            onUpdateOpen(newValue) {
              if (!newValue) this.onCancelAction();
            },
            closeDialog() {
              this.open = false;
              setTimeout(() => removeDialogMount(), 200);
            }
          },
          template: `
                        <cdx-dialog
                            v-model:open="open"
                            :title="$options.i18n.title"
                            :use-close-button="true"
                            @update:open="onUpdateOpen"
                            :primary-action="primaryAction"
                            :default-action="defaultAction"
                            @primary="onPrimaryAction"
                            @default="onCancelAction"
                            class="vgtn-diff-dialog review-tool-dialog"
                        >

                            <div class="review-tool-form-section vgtn-diff-dialog__content">
                                <div v-if="!diffHtml || diffHtml === ''" class="vgtn-diff-dialog__nodiff">
                                    {{ $options.i18n.noDiff }}
                                </div>
                                <div v-else class="vgtn-diff-dialog__diff" v-html="diffHtml"></div>
                            </div>

                        </cdx-dialog>
                    `
        });
        registerCodexComponents(app, Codex);
        mountApp(app);
      });
    } catch (error) {
      console.error("[VGTNTool] Failed to open diff dialog", error);
      mw && mw.notify && mw.notify(state_default.convByVar({
        hant: "無法開啟差異對話框。",
        hans: "无法开启差异对话框。"
      }), { type: "error", title: "[VGTNTool]" });
      throw error;
    }
  }

  // src/dom.ts
  var editorContainerId = "vgtn-editor";
  var addedSections = [];
  var $tableTemplate = null;
  function buildTable() {
    if (!$tableTemplate) {
      $tableTemplate = $("<table>").addClass("wikitable vgtn-editable-table tablesorter").append($("<thead>").append($("<tr>").append($("<th>").attr("scope", "col").text("原名")).append($("<th>").attr("scope", "col").data("sorter", false).text("相關連結")).append($("<th>").attr("scope", "col").data("sorter", false).text("中文名")).append($("<th>").attr("scope", "col").data("sorter", false).text("備註")))).append($("<tbody>"));
    }
    const $table = $tableTemplate.clone(true);
    setTableFooter($table);
    return $table;
  }
  function setTableRow($tbody, record = {}) {
    const $row = $("<tr>").addClass("vgtn-editable-row");
    $row.append($("<th>").attr("scope", "row").css({
      "font-weight": "normal",
      "text-align": "left"
    }).html(EntryNameField(record).html()));
    $row.append($("<td>").html(LinkVariableField(record).html()));
    $row.append($("<td>").html(LocaleVariableField(record).html()));
    $row.append($("<td>").html(CommentField(record).html()));
    $tbody.append($row);
    return $row;
  }
  function setTableFooter($table) {
    const $tfoot = $("<tfoot>");
    const $tfootRow = $("<tr>");
    const $tfootCell = $("<td>").attr("colspan", 4).css({ "text-align": "center" }).addClass("vgtn-table-footer");
    const $addRowButton = $("<a>").addClass("vgtn-add-row-btn").attr("href", "javascript:void(0)").text("新增行").on("click", (e) => {
      e.preventDefault();
      const $newRow = setTableRow($table.find("tbody"), {});
      $table.trigger("addRows", [$newRow]);
    });
    $tfootCell.append($addRowButton);
    $tfootRow.append($tfootCell);
    $tfoot.append($tfootRow);
    $table.append($tfoot);
  }
  function addSection($mwHeading2) {
    const $table = buildTable();
    setTableRow($table.find("tbody"));
    const $newHeadingSpan = $("<span>").addClass("vgtn-editable-span vgtn-new-section-name").attr("contenteditable", "plaintext-only").attr("placeholder", "(新章節)");
    const $h3 = $("<h3>").addClass("vgtn-new-section").append($newHeadingSpan);
    const $mwHeading3 = $("<div>").addClass("mw-heading mw-heading3").append($h3);
    setHeading3EditingButtons($mwHeading3, null, $table);
    $mwHeading2.after($mwHeading3);
    $mwHeading3.after($table);
    addedSections.push({ $h2: $mwHeading2.find("h2"), $h3 });
    setEditable($table);
  }
  function removeSection($mwHeading3) {
    const $h3 = $mwHeading3.find("h3");
    if ($h3.hasClass("vgtn-section-removed")) {
      $h3.removeClass("vgtn-section-removed");
      const $removeSectionButton = $mwHeading3.find(".mw-editsection .vgtn-remove-section-btn");
      $removeSectionButton.text("刪除章節");
    } else {
      $h3.addClass("vgtn-section-removed");
      const $removeSectionButton = $mwHeading3.find(".mw-editsection .vgtn-remove-section-btn");
      $removeSectionButton.text("取消刪除章節");
    }
  }
  async function editSection($mwHeading3) {
    const sectionTitle = $mwHeading3.find("h3").attr("id");
    const allData = await fetchModuleData();
    const entries = {};
    for (const mainIdx in allData) {
      const group = allData[mainIdx];
      if (typeof group !== "object" || !group.name) continue;
      for (const key in group) {
        if (key === "name") continue;
        const sub = group[key];
        const arr = [];
        for (const idx in sub) {
          if (idx === "name") continue;
          arr.push(sub[idx]);
        }
        entries[sub.name.replace(/\s/g, "_")] = arr;
      }
    }
    if (!entries[sectionTitle]) {
      mw.notify(`無法找到段落「${sectionTitle}」。請確認段落名稱是否正確。`, {
        type: "error",
        autoHide: true,
        autoHideSeconds: 3
      });
      return;
    }
    const $ogTable = $mwHeading3.next("table.wikitable");
    const $table = buildTable();
    $table.data("vgtn-section", sectionTitle);
    $ogTable.hide();
    $mwHeading3.after($table);
    const $tbody = $table.find("tbody");
    for (let i = 0; i < entries[sectionTitle].length; i++) {
      let record = entries[sectionTitle][i];
      if (Array.isArray(record)) record = { "1": record[0] };
      setTableRow($tbody, record);
    }
    setHeading3EditingButtons($mwHeading3, $ogTable, $table);
    setEditable($table);
  }
  function setEditable($table) {
    $("[contenteditable]").on("paste", function(e) {
      const $self = $(this);
      setTimeout(function() {
        $self.html($self.text());
      }, 0);
    }).on("keypress", function(e) {
      return e.which !== 13;
    });
    $table.tablesorter({
      sortReset: true,
      sortStable: true,
      emptyTo: "bottom",
      textSorter: { 0: $.tablesorter.sortText },
      textExtraction: {
        0: (node, table, cellIndex) => {
          return $(node).find(".vgtn-record-name").text().trim().replace(/[\s_]/g, "").toLowerCase();
        }
      }
    });
  }
  async function saveChanges() {
    const { start, base, fulltext: oldText } = await fetchModuleText();
    const $editableTables = $(".vgtn-editable-table");
    if ($editableTables.length === 0) {
      console.error("[VGTNTool] 沒有找到編輯中的表格。");
      mw.notify("沒有找到編輯中的表格。", { type: "error", autoHide: true, autoHideSeconds: 3 });
      return;
    }
    let newText = oldText;
    if (addedSections.length > 0) {
      addedSections.forEach((addedSection) => {
        const { $h2, $h3 } = addedSection;
        const h2Title = $h2.attr("id");
        const h3Title = $h3.find(".vgtn-new-section-name").text().trim();
        if (!h3Title) {
          return;
        }
        const escapedSectionName = h3Title.replace(/"/g, '\\"');
        const escapedParentName = mw.util.escapeRegExp(h2Title).replace(/_/g, "[ _]");
        const parentRegex = new RegExp(`^(data\\[\\d+\\]=\\{\\s*\\-*\\s*name=['"]${escapedParentName}['"],\\s*\\-*\\r?\\n[\\s\\S]*?)(^\\})$`, "gm");
        if (parentRegex.test(newText)) {
          const newSectionText = `  {
    name="${escapedSectionName}",
    --------------------------------------------------------------------------
  },

`;
          newText = newText.replace(parentRegex, `$1${newSectionText}$2`);
          console.log(`[VGTNTool] 新增段落「${h3Title}」到「${h2Title}」。`);
        } else {
          console.error(`[VGTNTool] 無法找到父段落「${h2Title}」。無法新增段落「${h3Title}」。`);
          mw.notify(`無法找到父段落「${h2Title}」。無法新增段落「${h3Title}」。`, {
            type: "error",
            autoHide: true,
            autoHideSeconds: 3
          });
        }
      });
    }
    const $deletedSections = $(".vgtn-section-removed");
    if ($deletedSections.length > 0) {
      $deletedSections.each(function() {
        const $h3 = $(this);
        let sectionName;
        if ($h3.hasClass("vgtn-new-section")) {
          sectionName = $h3.find(".vgtn-new-section-name").text().trim();
        } else {
          sectionName = $h3.attr("id");
        }
        const escapedSectionName = mw.util.escapeRegExp(sectionName).replace(/_/g, "[ _]");
        const sectionRegex = new RegExp(`[\\r\\n]*^[ \\t]*\\{\\s*name=['"]${escapedSectionName}['"],\\s*\\-*\\r?\\n[\\s\\S]*?^[ \\t]*\\},$`, "gm");
        if (sectionRegex.test(newText)) {
          newText = newText.replace(sectionRegex, "");
          console.log(`[VGTNTool] 刪除段落「${sectionName}」。`);
        } else {
          console.error(`[VGTNTool] 無法找到段落「${sectionName}」。無法刪除。`);
        }
      });
    }
    $editableTables.each(function() {
      const $table = $(this);
      const $rows = $table.find("tbody tr");
      let sectionName = "";
      if ($table.data("vgtn-section")) {
        sectionName = $table.data("vgtn-section");
      } else {
        const $mwHeading3 = $table.prev(".mw-heading3");
        const $h3 = $mwHeading3.find("h3");
        if ($h3.hasClass("vgtn-section-removed")) {
          return;
        }
        sectionName = $h3.find(".vgtn-new-section-name").text().trim();
        if (!sectionName) {
          console.error("[VGTNTool] 段落名稱為空,無法儲存。請確認段落名稱是否已填寫。");
          mw.notify("新章節名稱為空,無法儲存。請確認新章節名稱是否已填寫。", {
            type: "error",
            autoHide: true,
            autoHideSeconds: 3
          });
          return;
        }
      }
      let sectionNewRows = "";
      $rows.each(function() {
        const $row = $(this);
        if ($row.hasClass("vgtn-row-removed")) {
          return;
        }
        const name = $row.find(".vgtn-record-name").text().trim();
        if (!name) {
          console.warn("[VGTNTool] 發現空名稱的行,將跳過此行。");
          return;
        }
        const lang = $row.find(".vgtn-record-lang").text().trim();
        const dab = $row.find(".vgtn-record-dab").text().trim();
        const hanja = $row.find(".vgtn-record-hanja").is(":checked");
        const rm = $row.find(".vgtn-record-rm").text().trim();
        const aliases = $row.find(".vgtn-record-aliases").text().trim().split("|").map((a) => a.trim()).filter((a) => a);
        const link = $row.find(".vgtn-record-link").text().trim();
        const iw = $row.find(".vgtn-record-iw").text().trim();
        const wd = $row.find(".vgtn-record-wd").text().trim();
        const namu = $row.find(".vgtn-record-namu").text().trim();
        const tw = $row.find(".vgtn-record-tw").text().trim();
        const hk = $row.find(".vgtn-record-hk").text().trim();
        const mo = $row.find(".vgtn-record-mo").text().trim();
        const cn = $row.find(".vgtn-record-cn").text().trim();
        const sg = $row.find(".vgtn-record-sg").text().trim();
        const my = $row.find(".vgtn-record-my").text().trim();
        const comment = $row.find(".vgtn-record-comment").text().trim();
        sectionNewRows += `    { "${name.replace(/"/g, '\\"')}"`;
        if (lang) sectionNewRows += `, lang="${lang}"`;
        if (dab) sectionNewRows += `, dab="${dab}"`;
        if (rm) sectionNewRows += `, rm="${rm}"`;
        if (cn) sectionNewRows += `, cn="${cn}"`;
        if (hk) sectionNewRows += `, hk="${hk}"`;
        if (tw) sectionNewRows += `, tw="${tw}"`;
        if (sg) sectionNewRows += `, sg="${sg}"`;
        if (mo) sectionNewRows += `, mo="${mo}"`;
        if (my) sectionNewRows += `, my="${my}"`;
        if (hanja) sectionNewRows += `, hanja=true`;
        if (link) sectionNewRows += `, link="${link}"`;
        if (iw) sectionNewRows += `, iw="${iw}"`;
        if (wd) sectionNewRows += `, wd="${wd}"`;
        if (namu) sectionNewRows += `, namu="${namu}"`;
        if (aliases.length > 0) sectionNewRows += `, aliases={ "${aliases.join('", "')}" }`;
        if (comment) sectionNewRows += `, comment="${comment}"`;
        sectionNewRows += " },\n";
      });
      const escapedSectionName = mw.util.escapeRegExp(sectionName).replace(/_/g, "[ _]");
      console.log(`[VGTNTool] escapedSectionName: ${escapedSectionName}`);
      const sectionRegex = new RegExp(`^([ \\t]*\\{\\s*name=['"]${escapedSectionName}['"],\\s*\\-*\\r?\\n)([\\s\\S]*?)(^[ \\t]*\\},)$`, "gm");
      if (sectionRegex.test(newText)) {
        newText = newText.replace(sectionRegex, `$1${sectionNewRows}$3`);
      } else {
        console.error(`[VGTNTool] 無法找到標題為「${sectionName}」的段落,可能被刪除了。sectionRegex:`, sectionRegex, `newText:`, newText);
      }
    });
    const $mwHeading2s = $(".mw-heading2");
    $mwHeading2s.each(function() {
      const $mwHeading2 = $(this);
      if ($mwHeading2.hasClass("vgtn-sort-asc") || $mwHeading2.hasClass("vgtn-sort-desc")) {
        const sortOrder = $mwHeading2.hasClass("vgtn-sort-asc") ? "asc" : "desc";
        const parentName = $mwHeading2.find("h2").attr("id");
        const escapedParentName = mw.util.escapeRegExp(parentName).replace(/_/g, "[\\s_]");
        const parentRegex = new RegExp(`^(data\\[\\d+\\]=\\{\\s*\\-*\\s*name=['"]${escapedParentName}['"],\\s*\\-*\\r?\\n)([\\s\\S]*?)(^\\})$`, "gm");
        if (parentRegex.test(newText)) {
          const parentContent = newText.match(parentRegex)[0];
          const sectionRegex = new RegExp(`^[ \\t]*\\{\\s*name=['"]([^'"]+)['"],\\s*\\-*\\r?\\n[\\s\\S]*?^[ \\t]*\\},$`, "gm");
          const matches = Array.from(parentContent.matchAll(sectionRegex));
          matches.sort((a, b) => {
            const nameA = a[1].toLowerCase().replace(/[\s_]/g, "");
            const nameB = b[1].toLowerCase().replace(/[\s_]/g, "");
            return sortOrder === "asc" ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
          });
          let sortedContent = "";
          matches.forEach((match) => {
            sortedContent += match[0] + "\n\n";
          });
          newText = newText.replace(parentRegex, `$1
${sortedContent}$3`);
          console.log(`[VGTNTool] 已對段落「${parentName}」進行排序。排序方式:${sortOrder === "asc" ? "升序" : "降序"}。`);
        } else {
          console.error(`[VGTNTool] 無法找到段落「${parentName}」。無法進行排序。`);
        }
      }
    });
    const diffHtml = await fetchDiffHtml(oldText, newText);
    showDiffDialog(diffHtml, newText, start, base);
  }
  function cancelEdit($ogTable, $table, $mwHeading3) {
    $table.remove();
    $ogTable.show();
    setHeading3Buttons($mwHeading3);
  }
  function EntryNameField(record) {
    var _a;
    const $editableField = $("<div>").addClass("vgtn-editable-field");
    const $nameSpan = $("<span>").data("vgtn-record", "name").addClass("vgtn-editable-span vgtn-record-name").css({ "white-space": "nowrap" }).attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record["1"] || "");
    $nameSpan.on("blur", function() {
      const $th = $(this).closest("th");
      $th.closest("table").trigger("updateCell", [$th]);
    });
    const $langCode = $("<code>").data("vgtn-record", "lang").addClass("vgtn-editable-code vgtn-record-lang").attr("contenteditable", "plaintext-only").attr("placeholder", "(lang)").text(record.lang || "");
    $langCode.on("input", function() {
      const lang = $(this).text().trim();
      const $hanjaBundle = $editableField.find(".vgtn-editable-hanja-bundle");
      const $rmSpanBundle2 = $editableField.find(".vgtn-editable-rm-bundle");
      const $namuSpanBundle = $editableField.closest("tr").find(".vgtn-editable-namu-bundle");
      if (lang === "ko") {
        $hanjaBundle.show();
        $namuSpanBundle.show();
      } else {
        $hanjaBundle.hide();
        $namuSpanBundle.hide();
      }
      if (["ja", "ko", "ru"].includes(lang)) $rmSpanBundle2.show();
      else $rmSpanBundle2.hide();
    });
    const $dabCode = $("<code>").data("vgtn-record", "dab").addClass("vgtn-editable-code vgtn-record-dab").attr("contenteditable", "plaintext-only").attr("placeholder", "(dab)").text(record.dab || "");
    const $removeRowButton = $("<a>").addClass("vgtn-remove-row-btn").attr("href", "javascript:void(0)").text("刪除行").on("click", (e) => {
      e.preventDefault();
      const $row = $editableField.closest("tr");
      if ($row.hasClass("vgtn-row-removed")) {
        $row.removeClass("vgtn-row-removed");
        $removeRowButton.text("刪除行");
      } else {
        $row.addClass("vgtn-row-removed");
        $removeRowButton.text("取消刪除行");
      }
    });
    const $removeRowButtonBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-remove-row-bundle").append($("<span>").text("[").css({ "padding-left": "0.2em" })).append($removeRowButton).append($("<span>").text("]"));
    $editableField.append($nameSpan).append(" ").append($langCode).append(" ").append($dabCode).append(" ").append($removeRowButtonBundle);
    const $hanjaCheckbox = $("<input>").attr("type", "checkbox").data("vgtn-record", "hanja").addClass("vgtn-editable-checkbox vgtn-record-hanja").prop("checked", record.hanja === true);
    const $hanjaCheckboxBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-hanja-bundle").append($("<br>")).append($("<label>").attr("for", "vgtn-record-hanja").addClass("vgtn-record-label").append("確認正字(非音譯): ")).append($hanjaCheckbox);
    $editableField.append($hanjaCheckboxBundle);
    if (!record.lang || record.lang !== "ko") {
      $hanjaCheckboxBundle.hide();
    }
    const $rmSpan = $("<span>").data("vgtn-record", "rm").addClass("vgtn-editable-span vgtn-record-rm").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record.rm || "");
    const $rmSpanBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-rm-bundle").append($("<br>")).append($("<label>").attr("for", "vgtn-record-rm").addClass("vgtn-record-label").append("羅馬字: ")).append($rmSpan);
    $editableField.append($rmSpanBundle);
    if (!record.lang || !["ja", "ko", "ru"].includes(record.lang)) {
      $rmSpanBundle.hide();
    }
    const $aliasesSpan = $("<span>").data("vgtn-record", "aliases").addClass("vgtn-editable-span vgtn-record-aliases").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(((_a = record.aliases) == null ? void 0 : _a.join("|")) || "");
    $editableField.append($("<br>")).append($("<label>").attr("for", "vgtn-record-aliases").addClass("vgtn-record-label").append("別名(豎線隔開): ")).append($aliasesSpan);
    return $editableField;
  }
  function LinkVariableField(record) {
    const $editableField = $("<div>").addClass("vgtn-editable-field");
    const linkNames = [
      ["link", "本站連結"],
      ["iw", "跨語言"],
      ["wd", "維基數據"],
      ["namu", "納木維基"]
    ];
    linkNames.forEach(([key, label]) => {
      const $editableSpan = $("<span>").data("vgtn-record", key).addClass("vgtn-editable-span vgtn-record-" + key).attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record[key] || "");
      const $editableSpanBundle = $("<span>").addClass("vgtn-editable-bundle vgtn-editable-" + key + "-bundle");
      if ($editableField.children().length > 0) $editableSpanBundle.append($("<br>"));
      $editableSpanBundle.append($("<label>").attr("for", "vgtn-record-" + key).addClass("vgtn-record-label").append(label + ": ")).append($editableSpan);
      $editableField.append($editableSpanBundle);
    });
    if (!record.lang || record.lang !== "ko") {
      $editableField.find(".vgtn-editable-namu-bundle").hide();
    }
    return $editableField;
  }
  function LocaleVariableField(record) {
    const $editableField = $("<div>").addClass("vgtn-editable-field");
    const locales = ["tw", "hk", "mo", "cn", "sg", "my"];
    const localeFallback = function(locale, record2) {
      const localeFallbacks = {
        "tw": ["hk", "mo", "cn", "sg", "my"],
        "hk": ["mo", "tw", "cn", "sg", "my"],
        "mo": ["hk", "tw", "cn", "sg", "my"],
        "cn": ["sg", "my", "tw", "hk", "mo"],
        "sg": ["my", "cn", "tw", "hk", "mo"],
        "my": ["sg", "cn", "tw", "hk", "mo"]
      };
      for (const fallback of localeFallbacks[locale]) {
        if (record2[fallback]) return record2[fallback];
      }
      return "(空)";
    };
    locales.forEach((locale) => {
      const $editableSpan = $("<span>").data("vgtn-record", locale).addClass("vgtn-editable-span vgtn-record-" + locale).attr("lang", "zh-" + locale).attr("contenteditable", "plaintext-only").attr("placeholder", localeFallback(locale, record)).text(record[locale] || "");
      $editableSpan.on("input", function() {
        const $editableSpans = $editableField.find(".vgtn-editable-span");
        const newRecord = {};
        $editableSpans.each(function() {
          newRecord[$(this).data("vgtn-record")] = $(this).text().trim();
        });
        $editableSpans.each(function() {
          $(this).attr("placeholder", localeFallback($(this).data("vgtn-record"), newRecord));
        });
      });
      if ($editableField.children().length > 0) $editableField.append($("<br>"));
      $editableField.append($("<label>").attr("for", "vgtn-record-" + locale).addClass("vgtn-record-label").append(locale + ": ")).append($editableSpan);
    });
    return $editableField;
  }
  function CommentField(record) {
    const $editableField = $("<div>").addClass("vgtn-editable-field");
    const $commentSpan = $("<span>").data("vgtn-record", "comment").addClass("vgtn-editable-span vgtn-record-comment").attr("contenteditable", "plaintext-only").attr("placeholder", "(空)").text(record.comment || "");
    $editableField.append($commentSpan);
    return $editableField;
  }
  function addEditButtons($content) {
    const $parser = $content.find(".mw-parser-output");
    if (document.getElementById(editorContainerId)) return;
    $("<div>").attr("id", editorContainerId).css("display", "none").appendTo($parser);
    $parser.find(".mw-heading3").each((i, el) => {
      const $mwHeading3 = $(el);
      setHeading3Buttons($mwHeading3);
    });
    $parser.find(".mw-heading2").each((i, el) => {
      const $mwHeading2 = $(el);
      if ($mwHeading2.find("h2").attr("id") === "参见") return;
      setHeading2Buttons($mwHeading2);
    });
  }
  function setHeading3Buttons($mwHeading3) {
    const $editButton = $("<a>").addClass("vgtn-edit-btn").attr("href", "javascript:void(0)").text("编辑").on("click", (e) => {
      e.preventDefault();
      editSection($mwHeading3);
    });
    if (!$mwHeading3.find(".mw-editsection").length) {
      $mwHeading3.append($("<span>").addClass("mw-editsection"));
    }
    const $editSection = $mwHeading3.find(".mw-editsection");
    $editSection.empty();
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
    $editSection.append($editButton);
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
  }
  function setHeading3EditingButtons($mwHeading3, $ogTable, $table) {
    const $saveButton = $("<a>").addClass("vgtn-save-btn").attr("href", "javascript:void(0)").text("儲存").on("click", async (e) => {
      e.preventDefault();
      await saveChanges();
    });
    const $cancelButton = $("<a>").addClass("vgtn-cancel-btn").attr("href", "javascript:void(0)").text("取消").on("click", (e) => {
      e.preventDefault();
      cancelEdit($ogTable, $table, $mwHeading3);
    });
    const $removeSectionButton = $("<a>").addClass("vgtn-remove-section-btn").attr("href", "javascript:void(0)").text("删除章節").on("click", (e) => {
      e.preventDefault();
      removeSection($mwHeading3);
    });
    if (!$mwHeading3.find(".mw-editsection").length) {
      $mwHeading3.append($("<span>").addClass("mw-editsection"));
    }
    const $editSection = $mwHeading3.find(".mw-editsection");
    $editSection.empty();
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
    $editSection.append($saveButton);
    if (!$mwHeading3.find("h3").hasClass("vgtn-new-section")) {
      $editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
      $editSection.append($cancelButton);
    }
    $editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
    $editSection.append($removeSectionButton);
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
  }
  function setHeading2Buttons($mwHeading2) {
    const $addButton = $("<a>").addClass("vgtn-add-btn").attr("href", "javascript:void(0)").text("新增章節").on("click", (e) => {
      e.preventDefault();
      addSection($mwHeading2);
    });
    const $sortButton = $("<a>").addClass("vgtn-sort-btn").attr("href", "javascript:void(0)").text("升序排序").on("click", (e) => {
      e.preventDefault();
      if ($mwHeading2.hasClass("vgtn-sort-asc")) {
        $mwHeading2.removeClass("vgtn-sort-asc").addClass("vgtn-sort-desc");
        $sortButton.text("原始排序");
      } else if ($mwHeading2.hasClass("vgtn-sort-desc")) {
        $mwHeading2.removeClass("vgtn-sort-desc");
        $sortButton.text("升序排序");
      } else {
        $mwHeading2.addClass("vgtn-sort-asc");
        $sortButton.text("降序排序");
      }
    });
    if (!$mwHeading2.find(".mw-editsection").length) {
      $mwHeading2.append($("<span>").addClass("mw-editsection"));
    }
    const $editSection = $mwHeading2.find(".mw-editsection");
    $editSection.empty();
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("["));
    $editSection.append($addButton);
    $editSection.append($("<span>").addClass("mw-editsection-separator").text(" | "));
    $editSection.append($sortButton);
    $editSection.append($("<span>").addClass("mw-editsection-bracket").text("]"));
  }

  // src/main.ts
  function injectStylesheet() {
    mw.util.addCSS(`
        .vgtn-section-removed {
            color: var(--lt-color-text-very-light, #8f96a3) !important;
            text-decoration: line-through;
        }
        .vgtn-section-removed .mw-editsection {
            color: var(--color-base, #202122) !important;
            display: inline-block;
        }
        .vgtn-record-label {
            font-size: small;
        }
        .vgtn-editable-remove-row-bundle {
            font-size: smaller;
            white-space: nowrap;
        }
        .vgtn-editable-span {
            padding-left: 0.2em;
            padding-right: 0.2em;
            border-bottom: 2px solid var(--lt-color-border-dark, #c2c9d6);
        }
        .vgtn-editable-span:focus {
            border-bottom: none;
        }
        .vgtn-editable-code {
            font-size: smaller;
            white-space: nowrap;
        }
        [contenteditable="plaintext-only"]:empty:before{
            content: attr(placeholder);
            pointer-events: none;
            color: var(--lt-color-text-very-light, #8f96a3);
        }
        .wikitable tbody tr th,
        .wikitable tbody tr td:nth-last-child(1) {
            max-width: 15em;
        }
        .vgtn-row-removed th .vgtn-editable-field:before {
            content: "✘";
            color: red;
            font-size: larger;
        }
        .vgtn-table-footer {
            font-size: smaller;
        }
        .tablesorter-headerUnSorted:not(.sorter-false) {
            background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_both.svg);
            cursor: pointer;
            background-repeat: no-repeat;
            background-position: center right;
            padding-right: 21px;
        }
        .tablesorter-headerAsc:not(.vgtn-table-footer) {
            background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_up.svg);
            cursor: pointer;
            background-repeat: no-repeat;
            background-position: center right;
            padding-right: 21px;
        }
        .tablesorter-headerDesc:not(.vgtn-table-footer) {
            background-image: url(/w/resources/src/jquery.tablesorter.styles/images/sort_down.svg);
            cursor: pointer;
            background-repeat: no-repeat;
            background-position: center right;
            padding-right: 21px;
        }
        .mw-heading2.vgtn-sort-asc .mw-editsection::after {
            content: url(/w/resources/src/jquery.tablesorter.styles/images/sort_up.svg);
            display: inline-block;
            width: 16px;
            position: relative;
            top: -.2em;
        }
        .mw-heading2.vgtn-sort-desc .mw-editsection::after {
            content: url(/w/resources/src/jquery.tablesorter.styles/images/sort_down.svg);
            display: inline-block;
            width: 16px;
            position: relative;
            top: -.2em;
        }
        .vgtn-diff-dialog-diff {
            padding: 10px;
            overflow-x: auto;
        }
    `);
  }
  function init() {
    if (mw.config.get("wgPageName") !== "WikiProject:电子游戏/译名表") return;
    mw.loader.load("mediawiki.diff.styles");
    console.log("[VGTNTool] 小工具已載入。");
    injectStylesheet();
    mw.hook("wikipage.content").add(($content) => addEditButtons($content));
  }
  init();
})();
// </nowiki>