跳转到内容

User:SuperGrey/gadgets/ReviewTool/main.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/ReviewTool]]
// <nowiki>
(() => {
  var __defProp = Object.defineProperty;
  var __defProps = Object.defineProperties;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  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));
  var __esm = (fn, res) => function __init() {
    return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
  };
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

  // src/state.ts
  var state_exports = {};
  __export(state_exports, {
    default: () => state_default,
    state: () => state
  });
  var State, state, state_default;
  var init_state = __esm({
    "src/state.ts"() {
      State = class {
        constructor() {
          // 簡繁轉換
          this.convByVar = function(langDict) {
            if (langDict && langDict.hant) {
              return langDict.hant;
            }
            return "繁簡轉換未初始化,且 langDict 無效!";
          };
          // 當前條目標題
          this.articleTitle = "";
          // 是否在Talk名字空間
          this.inTalkPage = false;
          // 評級類型
          this.assessmentType = "";
          // 用戶名
          this.userName = mw.config.get("wgUserName") || "Example";
          // MediaWiki API 實例
          this._api = null;
          // When a heading's review button is clicked, store the heading element here so
          // dialogs can determine which section to operate on.
          this.pendingReviewHeading = null;
          // 批註模式狀態
          this.annotationModeState = {};
        }
        initHanAssist() {
          return mw.loader.using("ext.gadget.HanAssist").then((require2) => {
            const { convByVar } = require2("ext.gadget.HanAssist");
            if (typeof convByVar === "function") {
              this.convByVar = convByVar;
            }
          });
        }
        getApi() {
          if (!this._api) {
            this._api = new mw.Api({ "User-Agent": "ReviewTool/1.0" });
          }
          return this._api;
        }
        isAnnotationModeActive(headingTitle) {
          return !!this.annotationModeState[headingTitle];
        }
        toggleAnnotationModeState(headingTitle) {
          const currentState = this.isAnnotationModeActive(headingTitle);
          this.annotationModeState[headingTitle] = !currentState;
        }
      };
      state = new State();
      state_default = state;
    }
  });

  // src/main.ts
  init_state();

  // src/styles.css
  var styles_default = ".review-tool-dialog .review-tool-suggested-criteria-grid {\r\n    display: grid;\r\n    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\r\n    gap: 0;\r\n    margin-top: 5px;\r\n}\r\n\r\n.review-tool-dialog .review-tool-form-section:not(:first-child) {\r\n    margin-top: 10px;\r\n}\r\n\r\n.review-tool-dialog.review-tool-check-writing-dialog {\r\n    /* Make dialog wider for denser layout */\r\n    max-width: 900px;\r\n    width: 90vw;\r\n}\r\n\r\n.review-tool-dialog .chapter-block {\r\n    border: 1px solid rgba(0, 0, 0, 0.08);\r\n    background: rgba(0, 0, 0, 0.02);\r\n    padding: 12px;\r\n    margin-bottom: 12px;\r\n    border-radius: 6px;\r\n}\r\n\r\n.review-tool-dialog .chapter-suggestions {\r\n    margin-top: 8px;\r\n}\r\n\r\n.review-tool-dialog .suggestion-row {\r\n    display: flex;\r\n    gap: 8px;\r\n    align-items: flex-start;\r\n    padding: 6px 0;\r\n}\r\n\r\n.review-tool-dialog .suggestion-row:last-child {\r\n    border-bottom: none;\r\n    margin-bottom: 0;\r\n}\r\n\r\n.review-tool-dialog .suggestion-bullet {\r\n    display: list-item;\r\n    color: #666;\r\n    list-style-position: inside;\r\n    padding-left: 5px;\r\n}\r\n\r\n.review-tool-dialog .suggestion-columns {\r\n    display: flex;\r\n    gap: 8px;\r\n    flex: 1 1 auto;\r\n    align-items: flex-start;\r\n}\r\n\r\n.review-tool-dialog .quote-col {\r\n    flex: 0 0 35%;\r\n}\r\n\r\n.review-tool-dialog .suggestion-col {\r\n    flex: 1 1 auto;\r\n}\r\n\r\n.review-tool-dialog textarea {\r\n    min-height: 32px;\r\n    max-height: 160px;\r\n    resize: vertical;\r\n}\r\n\r\n.review-tool-dialog .review-tool-edit-step textarea {\r\n    font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Courier New', monospace;\r\n    min-height: 180px;\r\n    max-height: calc(100vh - 220px);\r\n}\r\n\r\n.review-tool-dialog .quote-area textarea {\r\n    color: #008560;\r\n}\r\n\r\n.review-tool-dialog .suggestion-controls {\r\n    display: flex;\r\n    gap: 8px;\r\n    justify-content: flex-end;\r\n    margin-top: 3px;\r\n}\r\n\r\n/* Controls row that places the add-suggestion button and chapter-level controls on one line */\r\n.review-tool-dialog .row-controls {\r\n    display: flex;\r\n    gap: 8px;\r\n    align-items: center;\r\n    justify-content: space-between;\r\n    margin-top: 8px;\r\n}\r\n\r\n.review-tool-dialog .suggestion-add {\r\n    margin-top: 0;\r\n    /* align with chapter controls inside .row-controls */\r\n    display: flex;\r\n    justify-content: flex-start;\r\n}\r\n\r\n.review-tool-dialog .chapter-controls {\r\n    display: flex;\r\n    gap: 8px;\r\n    justify-content: flex-end;\r\n    margin-top: 0;\r\n}\r\n\r\n/* Compact appearance for icon-only buttons */\r\n.review-tool-dialog .cdx-button--icon-only {\r\n    padding: 4px;\r\n    display: inline-flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n}\r\n\r\n/* Ensure the icon span inherits reasonable size */\r\n.review-tool-dialog .cdx-button__icon {\r\n    width: 16px;\r\n    height: 16px;\r\n}\r\n\r\n/* Annotation UI styles */\r\n.review-tool-annotation-ui .sentence {\r\n    background: transparent;\r\n}\r\n.review-tool-annotation-ui .sentence:hover {\r\n    background: rgba(255, 235, 59, 0.12);\r\n}\r\n.review-tool-annotation-ui .annotation-badge {\r\n    display: inline-block;\r\n    background: #ffcc00;\r\n    color: #000;\r\n    border-radius: 10px;\r\n    padding: 0 6px;\r\n    font-size: 11px;\r\n    margin-left: 6px;\r\n}\r\n.review-tool-annotation-ui .floating-button {\r\n    background: #1976d2;\r\n    color: white;\r\n    border: none;\r\n    padding: 6px 8px;\r\n    border-radius: 4px;\r\n    box-shadow: 0 2px 6px rgba(0,0,0,0.2);\r\n    cursor: pointer;\r\n}\r\n\r\n/* Floating popup button: span has both classes on same element */\r\n.review-tool-annotation-ui.floating-button,\r\n.floating-button.review-tool-annotation-ui {\r\n    background: #1976d2;\r\n    color: #fff;\r\n    border: none;\r\n    padding: 6px 8px;\r\n    border-radius: 4px;\r\n    box-shadow: 0 2px 6px rgba(0,0,0,0.2);\r\n    cursor: pointer;\r\n    font-size: 13px;\r\n    line-height: 1.2;\r\n    white-space: nowrap;\r\n}\r\n.review-tool-annotation-ui.floating-button:hover {\r\n    background: #1e88e5;\r\n}\r\n\r\n/* Annotation badge actual class name fix */\r\n.review-tool-annotation-badge {\r\n    display: inline-block;\r\n    background: #ffcc00;\r\n    color: #000;\r\n    border-radius: 10px;\r\n    padding: 0 6px;\r\n    font-size: 11px;\r\n    margin-left: 4px;\r\n    vertical-align: baseline;\r\n    text-decoration: none;\r\n}\r\n.review-tool-annotation-badge:hover {\r\n    filter: brightness(1.1);\r\n}\r\n.review-tool-annotation-badge--section {\r\n    margin-left: 6px;\r\n}\r\n\r\n.review-tool-inline-annotation {\r\n    display: inline-flex;\r\n    align-items: center;\r\n    margin-left: 4px;\r\n    vertical-align: baseline;\r\n    gap: 2px;\r\n}\r\n\r\n.review-tool-inline-annotation__icon {\r\n    background: #fff7d1;\r\n    border: 1px solid #f5c400;\r\n    border-radius: 999px;\r\n    color: #202122;\r\n    cursor: pointer;\r\n    font-size: 11px;\r\n    line-height: 1.3;\r\n    padding: 0 6px;\r\n    text-decoration: none;\r\n    display: inline-flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    min-height: 16px;\r\n}\r\n\r\n.review-tool-inline-annotation__icon:hover {\r\n    background: #ffe58f;\r\n}\r\n\r\nhtml:not(.review-tool-annotation-mode) .review-tool-inline-annotation {\r\n    display: none;\r\n}\r\n\r\n.review-tool-annotation-editor__label {\r\n    font-size: 12px;\r\n    font-weight: 600;\r\n    color: #54595d;\r\n    margin-bottom: 4px;\r\n}\r\n\r\n.review-tool-annotation-editor__section {\r\n    font-size: 13px;\r\n    color: #202122;\r\n}\r\n\r\n.review-tool-annotation-editor__quote {\r\n    background: #f8f9fa;\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    padding: 8px;\r\n    white-space: pre-wrap;\r\n    max-height: 160px;\r\n    overflow-y: auto;\r\n}\r\n\r\n.review-tool-annotation-editor__error {\r\n    color: #d73333;\r\n    font-size: 12px;\r\n    margin-top: 4px;\r\n}\r\n\r\n.review-tool-annotation-editor__footer {\r\n    display: flex;\r\n    justify-content: space-between;\r\n    align-items: center;\r\n    gap: 12px;\r\n    padding-top: 12px;\r\n}\r\n\r\n.review-tool-annotation-editor__actions {\r\n    display: flex;\r\n    gap: 8px;\r\n}\r\n\r\n.review-tool-annotation-viewer__empty {\r\n    text-align: center;\r\n    color: #54595d;\r\n    padding: 32px 0;\r\n}\r\n\r\n.review-tool-annotation-viewer__footer {\r\n    display: flex;\r\n    justify-content: space-between;\r\n    align-items: center;\r\n    gap: 12px;\r\n    padding-top: 12px;\r\n    flex-wrap: wrap;\r\n}\r\n\r\n.review-tool-annotation-viewer__footer-left {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 8px;\r\n    flex: 1 1 240px;\r\n}\r\n\r\n.review-tool-annotation-viewer__sort-select {\r\n    min-width: 200px;\r\n    flex: 0 0 220px;\r\n}\r\n\r\n.review-tool-annotation-viewer__footer-actions {\r\n    display: flex;\r\n    gap: 8px;\r\n    justify-content: flex-end;\r\n    flex: 1 1 auto;\r\n    flex-wrap: wrap;\r\n}\r\n\r\n.review-tool-annotation-viewer__section {\r\n    margin-bottom: 16px;\r\n}\r\n\r\n.review-tool-annotation-viewer__section-title {\r\n    font-size: 13px;\r\n    font-weight: 600;\r\n    margin: 0 0 6px;\r\n}\r\n\r\n.review-tool-annotation-viewer__items {\r\n    list-style: none;\r\n    padding: 0;\r\n    margin: 0;\r\n}\r\n\r\n.review-tool-annotation-viewer__item {\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 6px;\r\n    padding: 8px;\r\n    margin-bottom: 8px;\r\n    background: #fff;\r\n}\r\n\r\n.review-tool-annotation-viewer__quote {\r\n    font-style: italic;\r\n    color: #54595d;\r\n}\r\n\r\n.review-tool-annotation-viewer__opinion {\r\n    margin-top: 4px;\r\n}\r\n\r\n.review-tool-annotation-viewer__meta {\r\n    font-size: 12px;\r\n    color: #72777d;\r\n    margin-top: 4px;\r\n}\r\n\r\n.review-tool-annotation-viewer__actions {\r\n    display: flex;\r\n    gap: 8px;\r\n    margin-top: 6px;\r\n}\r\n\r\n/* Sentence highlight styles adapt to current DOM: spans carry both classes */\r\n.review-tool-annotation-ui.sentence {\r\n    background: transparent;\r\n    transition: background 120ms ease-in;\r\n}\r\n.review-tool-annotation-ui.sentence:hover {\r\n    background: rgba(255, 235, 59, 0.2);\r\n}\r\n/* Keep descendant version for future container-based refactor */\r\n.review-tool-annotation-ui .sentence:hover {\r\n    background: rgba(255, 235, 59, 0.2);\r\n}\r\n\r\n/* Cursor behavior for annotation sentences */\r\n.review-tool-annotation-ui.sentence { cursor: pointer; }\r\nhtml.rt-selecting .review-tool-annotation-ui.sentence { cursor: text; }\r\n\r\n/* Ensure floating button is always clickable */\r\n.review-tool-annotation-ui.floating-button,\r\n.floating-button.review-tool-annotation-ui { cursor: pointer; }\r\n\r\n/* Stronger highlight rule to ensure visibility on pages with competing styles */\r\n.review-tool-annotation-ui.sentence:hover,\r\n.review-tool-annotation-ui .sentence:hover {\r\n    background: rgba(255, 235, 59, 0.22) !important;\r\n}\r\n\r\n/* While annotation mode is active, allow selection over known inline editor widgets\r\n   that set `user-select: none` (e.g. ipe quick-edit buttons). This only applies\r\n   while our mode is on to avoid changing page behavior permanently. */\r\n.review-tool-annotation-mode .ipe__in-article-link,\r\n.review-tool-annotation-mode .ipe-quick-edit,\r\n.review-tool-annotation-mode .ipe-quick-edit--create-only,\r\n.review-tool-annotation-mode .qeec-ref-tag-copy-btn {\r\n    user-select: text !important;\r\n    pointer-events: auto !important;\r\n}\r\n\r\n.review-tool-multistep-dialog header {\r\n    padding: 16px 24px;\r\n}\r\n.review-tool-multistep-dialog header h2 {\r\n    margin: 0;\r\n    padding: 0;\r\n    font-size: 20px;\r\n}\r\n.review-tool-multistep-dialog__header-top {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: space-between;\r\n}\r\n.review-tool-multistep-dialog__stepper {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 12px;\r\n    padding: 8px 24px 0 24px;\r\n}\r\n.review-tool-multistep-dialog__stepper__label {\r\n    color: #6b7280; /* subtle gray */\r\n    font-size: 13px;\r\n}\r\n.review-tool-multistep-dialog__stepper__steps {\r\n    display: flex;\r\n    gap: 8px;\r\n}\r\n.review-tool-multistep-dialog__stepper__step {\r\n    background-color: #c8ccd1;\r\n    display: block;\r\n    width: 12px;\r\n    height: 12px;\r\n    border-radius: 999px;\r\n}\r\n.review-tool-multistep-dialog__stepper__step--active {\r\n    background-color: #0b5fff; /* accent */\r\n}\r\n.review-tool-multistep-dialog__image {\r\n    background-color: #f1f5f9;\r\n    display: flex;\r\n    justify-content: center;\r\n    padding: 12px 0;\r\n}\r\n.review-tool-multistep-dialog__image img { display: block; max-width: 100%; }\r\n.review-tool-multistep-dialog__text {\r\n    padding: 16px 24px;\r\n}\r\n.review-tool-multistep-dialog__text p { margin: 8px 0 0 0; font-size: 13px; }\r\n.review-tool-multistep-dialog__text ul { margin: 0; padding-left: 24px; }\r\n.review-tool-multistep-dialog__text li { font-size: 13px; }\r\n.review-tool-multistep-dialog footer {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: space-between;\r\n    border-top: 1px solid rgba(0,0,0,0.06);\r\n    padding: 12px 24px;\r\n}\r\n.review-tool-multistep-dialog__footer-left {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 8px;\r\n}\r\n.review-tool-multistep-dialog__actions { display: flex; gap: 12px; }\r\n\r\n.review-tool-preview-pre--html {\r\n    border: 1px solid rgba(0, 0, 0, 0.1);\r\n    border-radius: 6px;\r\n    background: #fafafa;\r\n    padding: 12px;\r\n}";

  // src/dom/talk_page.ts
  init_state();

  // src/templates.ts
  init_state();
  function assessments() {
    return {
      "bplus": {
        label: state_default.convByVar({ hant: "乙上級", hans: "乙上级" }),
        section_regex: /乙上?[級级][評评][審审選选級级]/,
        suggested_criteria: [
          state_default.convByVar({ hant: "來源", hans: "来源" }) + " (B1)",
          state_default.convByVar({ hant: "覆蓋面", hans: "覆盖面" }) + " (B2)",
          state_default.convByVar({ hant: "結構", hans: "结构" }) + " (B3)",
          state_default.convByVar({ hant: "文筆", hans: "文笔" }) + " (B4)",
          state_default.convByVar({ hant: "配圖", hans: "配图" }) + " (B5)",
          state_default.convByVar({ hant: "易讀", hans: "易读" }) + " (B6)",
          state_default.convByVar({ hant: "乙上級以外的建議", hans: "乙上级以外的建议" })
        ]
      },
      "good": {
        label: state_default.convByVar({ hant: "優良級", hans: "优良级" }),
        section_regex: /[優优]良(?:[級级]|[條条]目)[評评][審审選选級级]/,
        page_prefix: "Wikipedia:優良條目評選",
        suggested_criteria: [
          state_default.convByVar({ hant: "文筆", hans: "文笔" }) + " (GA1)",
          state_default.convByVar({ hant: "來源", hans: "来源" }) + " (GA2)",
          state_default.convByVar({ hant: "覆蓋面", hans: "覆盖面" }) + " (GA3)",
          state_default.convByVar({ hant: "中立", hans: "中立" }) + " (GA4) & " + state_default.convByVar({
            hant: "穩定",
            hans: "稳定"
          }) + " (GA5)",
          state_default.convByVar({ hant: "配圖", hans: "配图" }) + " (GA6)",
          state_default.convByVar({ hant: "結構", hans: "结构" }) + " (B3)",
          state_default.convByVar({ hant: "易讀", hans: "易读" }) + " (B6)",
          state_default.convByVar({ hant: "優良級以外的建議", hans: "优良级以外的建议" })
        ]
      },
      "a": {
        label: state_default.convByVar({ hant: "甲級", hans: "甲级" }),
        section_regex: /甲[級级][評评][審审選选級级]/,
        suggested_criteria: [
          state_default.convByVar({ hant: "來源", hans: "来源" }) + " (A1)",
          state_default.convByVar({ hant: "覆蓋面", hans: "覆盖面" }) + " (A2)",
          state_default.convByVar({ hant: "結構", hans: "结构" }) + " (A3)",
          state_default.convByVar({ hant: "文筆", hans: "文笔" }) + " (A4)",
          state_default.convByVar({ hant: "配圖", hans: "配图" }) + " (A5)",
          state_default.convByVar({ hant: "易讀", hans: "易读" }) + " (A6)",
          state_default.convByVar({ hant: "甲級以外的建議", hans: "甲级以外的建议" })
        ]
      },
      "featured": {
        label: state_default.convByVar({ hant: "典範級", hans: "典范级" }),
        section_regex: /典[範范](?:[級级]|[條条]目)[評评][審审選选級级]/,
        page_prefix: "Wikipedia:典范条目评选",
        suggested_criteria: [
          state_default.convByVar({ hant: "文筆", hans: "文笔" }) + " (FA1a)",
          state_default.convByVar({ hant: "覆蓋面", hans: "覆盖面" }) + " (FA1b)",
          state_default.convByVar({ hant: "來源", hans: "来源" }) + " (FA1c)",
          state_default.convByVar({ hant: "中立", hans: "中立" }) + " (FA1d) & " + state_default.convByVar({
            hant: "穩定",
            hans: "稳定"
          }) + " (FA1e)",
          state_default.convByVar({ hant: "格式", hans: "格式" }) + " (FA2)",
          state_default.convByVar({ hant: "結構", hans: "结构" }) + " (FA2b)",
          state_default.convByVar({ hant: "配圖", hans: "配图" }) + " (FA3)",
          state_default.convByVar({ hant: "長度", hans: "长度" }) + " (FA4)",
          state_default.convByVar({ hant: "易讀", hans: "易读" }) + " (A6)",
          state_default.convByVar({ hant: "典範級以外的建議", hans: "典范级以外的建议" })
        ]
      },
      "featured_list": {
        label: state_default.convByVar({ hant: "特色列表級", hans: "特色列表级" }),
        section_regex: /特色列表[評评][審审選选級级]/,
        page_prefix: "Wikipedia:特色列表評选",
        suggested_criteria: [
          state_default.convByVar({ hant: "文筆", hans: "文笔" }) + " (FL1)",
          state_default.convByVar({ hant: "序言", hans: "序言" }) + " (FL2)",
          state_default.convByVar({ hant: "覆蓋面", hans: "覆盖面" }) + " (FL3a)",
          state_default.convByVar({ hant: "長度", hans: "长度" }) + " (FL3b)",
          state_default.convByVar({ hant: "結構", hans: "结构" }) + " (FL4)",
          state_default.convByVar({ hant: "格式", hans: "格式" }) + " (FL5a)",
          state_default.convByVar({ hant: "配圖", hans: "配图" }) + " (FL5b)",
          state_default.convByVar({ hant: "穩定", hans: "稳定" }) + " (FL6)",
          state_default.convByVar({ hant: "特色列表級以外的建議", hans: "特色列表级以外的建议" })
        ]
      }
    };
  }
  function getSectionRegexes() {
    const regexes = {};
    for (const [key, assessment] of Object.entries(assessments())) {
      regexes[key] = assessment.section_regex;
    }
    return regexes;
  }
  function getAssessmentLabels() {
    const labels = {};
    for (const [key, assessment] of Object.entries(assessments())) {
      labels[key] = assessment.label;
    }
    return labels;
  }

  // src/dialogs/review_management.ts
  init_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/api.ts
  init_state();
  function parseQueryParams(url) {
    const qIdx = url.indexOf("?");
    const query = qIdx >= 0 ? url.slice(qIdx + 1) : url;
    const pairs = query.split("&").filter(Boolean);
    const out = {};
    for (const p of pairs) {
      const [k, v] = p.split("=");
      if (!k) continue;
      try {
        out[decodeURIComponent(k)] = v ? decodeURIComponent(v) : "";
      } catch (e) {
        out[k] = v || "";
      }
    }
    return out;
  }
  function findSectionInfoFromHeading(headingEl) {
    if (!headingEl) return { pageTitle: null, sectionId: null };
    const link = headingEl.querySelector("a.qe-target") || headingEl.querySelector('a[href*="action=edit"]');
    if (!link || !link.getAttribute) return { pageTitle: null, sectionId: null };
    const href = link.getAttribute("href") || "";
    const params = parseQueryParams(href);
    const title = params["title"] ? decodeURIComponent(params["title"]) : null;
    let sectionId = null;
    if (params["section"]) {
      const n = parseInt(params["section"], 10);
      if (!isNaN(n)) sectionId = n;
    }
    return { pageTitle: title, sectionId };
  }
  function createHeaderMarkup(title, level) {
    if (!title) return "";
    const eq = "=".repeat(Math.max(1, Math.min(6, level)));
    return `
${eq}${title}${eq}`;
  }
  function appendTextToSection(pageTitle, sectionId, appendText, summary) {
    return new Promise((resolve, reject) => {
      if (!pageTitle || typeof sectionId !== "number" || isNaN(sectionId)) {
        reject(new Error("Invalid pageTitle or sectionId"));
        return;
      }
      const api = state_default.getApi();
      const params = {
        action: "edit",
        title: pageTitle,
        section: sectionId,
        appendtext: appendText,
        format: "json",
        formatversion: 2
      };
      if (summary) params.summary = summary;
      api.postWithToken("csrf", params).done((res) => {
        if (res.edit && res.edit.result === "Success") {
          console.log("[ReviewTool][appendTextToSection] Append successful");
          mw.notify(state_default.convByVar({
            hant: "已成功將內容附加到指定段落。",
            hans: "已成功将内容附加到指定段落。"
          }));
          refreshPage();
        } else if (res.error && res.error.code === "editconflict") {
          console.error("[ReviewTool][appendTextToSection] Edit conflict occurred");
          mw.notify(state_default.convByVar({
            hant: "附加內容時發生編輯衝突。請重新嘗試。",
            hans: "附加内容时发生编辑冲突。请重新尝试。"
          }));
        } else {
          console.error("[ReviewTool][appendTextToSection] Append failed", res);
          mw.notify(state_default.convByVar({
            hant: "附加內容失敗。請稍後再試。",
            hans: "附加内容失败。请稍后再试。"
          }));
        }
      }).fail((err) => reject(err));
    });
  }
  function refreshPage() {
    setTimeout(() => {
      location.reload();
    }, 2e3);
  }
  function retrieveFullText(pageTitle, sectionId) {
    return new Promise((resolve, reject) => {
      if (!pageTitle) {
        reject(new Error("Invalid pageTitle"));
        return;
      }
      const api = state_default.getApi();
      const params = {
        action: "query",
        prop: "revisions",
        titles: pageTitle,
        rvslots: "main",
        rvprop: ["timestamp", "content"],
        curtimestamp: 1,
        format: "json",
        formatversion: 2
      };
      if (typeof sectionId === "number" && !isNaN(sectionId)) {
        params.rvsection = sectionId;
      }
      api.postWithToken("csrf", params).done((res) => {
        var _a, _b, _c, _d;
        try {
          const page = (_b = (_a = res == null ? void 0 : res.query) == null ? void 0 : _a.pages) == null ? void 0 : _b[0];
          const revision = (_c = page == null ? void 0 : page.revisions) == null ? void 0 : _c[0];
          if (revision) {
            const mainSlot = ((_d = revision.slots) == null ? void 0 : _d.main) || {};
            const text = typeof mainSlot.content === "string" ? mainSlot.content : mainSlot["*"] || "";
            resolve({
              text,
              starttimestamp: (res == null ? void 0 : res.curtimestamp) || "",
              basetimestamp: revision.timestamp || ""
            });
            return;
          }
        } catch (err) {
          reject(err);
          return;
        }
        reject(new Error("No content found"));
      }).fail((error) => reject(error));
    });
  }
  function replaceSectionText(pageTitle, sectionId, newText, summary, timestamps) {
    return new Promise(async (resolve, reject) => {
      try {
        if (!pageTitle || typeof sectionId !== "number" || isNaN(sectionId)) {
          reject(new Error("Invalid pageTitle or sectionId"));
          return;
        }
        const api = state_default.getApi();
        let starttimestamp = timestamps == null ? void 0 : timestamps.starttimestamp;
        let basetimestamp = timestamps == null ? void 0 : timestamps.basetimestamp;
        if (!starttimestamp || !basetimestamp) {
          const fetched = await retrieveFullText(pageTitle, sectionId);
          starttimestamp = fetched.starttimestamp;
          basetimestamp = fetched.basetimestamp;
        }
        const params = {
          action: "edit",
          title: pageTitle,
          section: sectionId,
          text: newText,
          starttimestamp,
          basetimestamp,
          format: "json",
          formatversion: 2
        };
        if (summary) params.summary = summary;
        api.postWithToken("csrf", params).done((data) => {
          var _a;
          if (((_a = data == null ? void 0 : data.edit) == null ? void 0 : _a.result) === "Success") {
            refreshPage();
          }
          resolve(data);
        }).fail((err) => reject(err));
      } catch (error) {
        reject(error);
      }
    });
  }
  function parseWikitextToHtml(wikitext, title) {
    return new Promise((resolve, reject) => {
      try {
        const api = state_default.getApi();
        const params = { action: "parse", text: wikitext || "", contentmodel: "wikitext", format: "json" };
        if (title) params.title = title;
        api.post(params).done((data) => {
          try {
            if (data && data.parse && data.parse.text) {
              resolve(data.parse.text["*"] || "");
              return;
            }
          } catch (e) {
          }
          resolve("");
        }).fail((err) => reject(err));
      } catch (e) {
        reject(e);
      }
    });
  }
  function compareWikitext(oldWikitext, newWikitext) {
    return new Promise((resolve, reject) => {
      try {
        const api = state_default.getApi();
        const params = {
          action: "compare",
          fromslots: "main",
          "fromtext-main": oldWikitext || "",
          fromtitle: mw.config.get("wgPageName"),
          frompst: "true",
          toslots: "main",
          "totext-main": newWikitext || "",
          totitle: mw.config.get("wgPageName"),
          topst: "true"
        };
        api.postWithToken("csrf", params).done((res) => {
          try {
            if (res && res.compare && res.compare["*"]) {
              resolve('<table class="diff"><colgroup><col class="diff-marker"/><col class="diff-content"/><col class="diff-marker"/><col class="diff-content"/></colgroup>' + res.compare["*"] + "</table>");
            }
            resolve(state_default.convByVar({ hant: "無差異。", hans: "无差异。" }));
          } catch (e) {
            reject(e);
          }
        }).fail((err) => reject(err));
      } catch (e) {
        reject(e);
      }
    });
  }

  // src/dialogs/utils.ts
  function afterServerHtmlInjected(targetEl, html) {
    if (!targetEl || !html) return;
    try {
      if (typeof mw !== "undefined" && mw && mw.hook && typeof mw.hook === "function") {
        const $ = window.jQuery;
        mw.hook("wikipage.content").fire($ ? $(targetEl) : targetEl);
      }
    } catch (e) {
      try {
        mw && mw.hook && mw.hook("wikipage.content").fire(targetEl);
      } catch (err) {
      }
    }
    if (html.indexOf('class="diff') !== -1) {
      try {
        mw && mw.loader && mw.loader.load && mw.loader.load("mediawiki.diff.styles");
      } catch (e) {
      }
    }
  }
  function triggerDialogContentHooks(vm, kind) {
    vm.$nextTick(() => {
      const htmlProp = kind === "preview" ? "previewHtml" : "diffHtml";
      const html = vm[htmlProp];
      if (!html) return;
      const refName = kind === "preview" ? "previewHtmlHost" : "diffHtmlHost";
      const refs = vm.$refs;
      let host = refs[refName];
      if (Array.isArray(host)) host = host[0];
      if (!host) return;
      if (!host.innerHTML || !host.innerHTML.trim()) {
        host.innerHTML = html;
      }
      afterServerHtmlInjected(host, html);
    });
  }
  function ensureDialogStepContentHooks(vm, handlers) {
    vm.$nextTick(() => {
      var _a, _b;
      const previewStep = (_a = handlers == null ? void 0 : handlers.previewStepIndex) != null ? _a : 1;
      const diffStep = (_b = handlers == null ? void 0 : handlers.diffStepIndex) != null ? _b : 2;
      if (vm.currentStep === previewStep && vm.previewHtml) {
        triggerDialogContentHooks(vm, "preview");
      } else if (vm.currentStep === diffStep && vm.diffHtml) {
        triggerDialogContentHooks(vm, "diff");
      }
    });
  }
  function advanceDialogStep(vm, handlers) {
    var _a;
    const totalSteps = (_a = handlers.totalSteps) != null ? _a : 3;
    if (vm.currentStep >= totalSteps - 1) return false;
    const nextStep = vm.currentStep + 1;
    vm.currentStep = nextStep;
    const runHandlers = () => {
      if (nextStep === 1 && handlers.onEnterEditStep) {
        handlers.onEnterEditStep.call(vm);
      } else if (nextStep === 2 && handlers.onEnterPreviewStep) {
        handlers.onEnterPreviewStep.call(vm);
      } else if (nextStep === 3 && handlers.onEnterDiffStep) {
        handlers.onEnterDiffStep.call(vm);
      }
      ensureDialogStepContentHooks(vm, handlers);
    };
    if (typeof vm.$nextTick === "function") {
      vm.$nextTick(runHandlers);
    } else {
      runHandlers();
    }
    return true;
  }
  function regressDialogStep(vm) {
    if (vm.currentStep <= 0) return false;
    vm.currentStep--;
    ensureDialogStepContentHooks(vm);
    return true;
  }

  // src/dialogs/review_management.ts
  function createReviewManagementDialog() {
    loadCodexAndVue().then(({ Vue, Codex }) => {
      const app = Vue.createMwApp({
        i18n: {
          submitting: state_default.convByVar({ hant: "添加中…", hans: "添加中…" }),
          submit: state_default.convByVar({ hant: "添加", hans: "添加" }),
          cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
          dialogTitle: state_default.convByVar({
            hant: "為「",
            hans: "为「"
          }) + state_default.articleTitle + state_default.convByVar({ hant: "」添加評審意見", hans: "」添加评审意见" }),
          submitUnderOpinionSubsection: state_default.convByVar({
            hant: "將評審內容置於「",
            hans: "将评审内容置于「"
          }) + state_default.userName + state_default.convByVar({ hant: "的意見」小節內", hans: "的意见」小节内" }),
          selectCriterion: state_default.convByVar({ hant: "評審標準:", hans: "评审标准:" }),
          criterionPlaceholder: state_default.convByVar({ hant: "選擇評審標準", hans: "选择评审标准" }),
          addCriteriaToReview: state_default.convByVar({ hant: "將以下標準加入評審", hans: "将以下标准加入评审" }),
          next: state_default.convByVar({ hant: "下一步", hans: "下一步" }),
          previous: state_default.convByVar({ hant: "上一步", hans: "上一步" }),
          previewHeading: state_default.convByVar({ hant: "預覽", hans: "预览" }),
          diffHeading: state_default.convByVar({ hant: "差異", hans: "差异" }),
          editHeading: state_default.convByVar({ hant: "編輯建議", hans: "编辑建议" }),
          editInstruction: state_default.convByVar({ hant: "在此調整要新增的維基語法內容,再前往預覽或差異。", hans: "在此调整要新增的维基语法内容,再前往预览或差异。" }),
          editPlaceholder: state_default.convByVar({ hant: "在此輸入或修改評審建議的維基語法內容…", hans: "在此输入或修改评审建议的维基语法内容…" }),
          noSpecificCriteria: state_default.convByVar({ hant: "未選擇評審標準。", hans: "未选择评审标准。" }),
          noDiff: state_default.convByVar({ hant: "無差異。", hans: "无差异。" })
        },
        data() {
          return {
            open: true,
            isSubmitting: false,
            currentStep: 0,
            submitUnderOpinionSubsection: !state_default.inTalkPage,
            // selected assessment type (e.g. 'bplus', 'good', ...)
            selectedAssessmentType: state_default.assessmentType || "",
            // selected suggested criteria (array of strings)
            selectedCriteria: [],
            // selected specific criterion (if needed)
            criterion: null,
            previewHtml: "",
            diffHtml: "",
            previewWikitext: "",
            existingSectionText: "",
            pendingNewSectionText: "",
            sectionRevisionInfo: null,
            editedDraft: ""
          };
        },
        computed: {
          assessmentLabels() {
            return getAssessmentLabels();
          },
          // options shaped for Codex CdxSelect (MenuItemData: { value, label })
          codexOptions() {
            return Object.entries(this.assessmentLabels).map(([type, label]) => ({ value: type, label }));
          },
          suggestedCriteria() {
            if (!this.selectedAssessmentType) return [];
            const a = assessments()[this.selectedAssessmentType];
            return a ? a.suggested_criteria || [] : [];
          },
          // shaped items for Codex checkbox list
          codexCriteriaItems() {
            return this.suggestedCriteria.map((item) => ({ value: item, label: item }));
          },
          primaryAction() {
            if (this.currentStep < 3) {
              return { label: this.$options.i18n.next || "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 || "Previous" };
            return { label: this.$options.i18n.cancel };
          }
        },
        methods: {
          triggerContentHooks(kind) {
            triggerDialogContentHooks(this, kind);
          },
          getPendingReviewSectionInfo() {
            const headingEl = state_default.pendingReviewHeading || null;
            const sec = findSectionInfoFromHeading(headingEl);
            const pageTitleToUse = sec && sec.pageTitle ? sec.pageTitle : state_default.articleTitle || "";
            const sectionIdToUse = typeof (sec && sec.sectionId) === "number" ? sec.sectionId : sec && sec.sectionId != null ? sec.sectionId : null;
            return { headingEl, sec, pageTitleToUse, sectionIdToUse };
          },
          buildHeadersForCriteria(level) {
            if (Array.isArray(this.selectedCriteria) && this.selectedCriteria.length > 0) {
              return this.selectedCriteria.map((item) => createHeaderMarkup(String(item), level)).join("");
            }
            if (this.criterion) {
              return createHeaderMarkup(String(this.criterion), level);
            }
            return "";
          },
          buildOpinionContent(level) {
            const criteriaContent = this.buildHeadersForCriteria(level);
            return criteriaContent && criteriaContent.trim() ? criteriaContent : "";
          },
          reportMissingOpinionEntries(showAlert = false) {
            const msg = state_default.convByVar({ hant: "請先選擇或輸入評審子項,再提交。", hans: "请先选择或输入评审子项,再提交。" });
            mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
            if (showAlert) {
              try {
                alert(msg);
              } catch (e) {
              }
            }
            return msg;
          },
          prepareEditDraft() {
            this.editedDraft = this.buildDraftContent().trim();
          },
          buildDraftContent() {
            const level = this.submitUnderOpinionSubsection ? 4 : 3;
            if (this.submitUnderOpinionSubsection) {
              return this.buildOpinionContent(level);
            }
            return this.buildHeadersForCriteria(level);
          },
          getDraftFragment() {
            const rawDraft = (this.editedDraft || "").trim();
            if (!rawDraft) return "";
            return `
${rawDraft}`;
          },
          preparePreviewContent() {
            const { pageTitleToUse, sectionIdToUse } = this.getPendingReviewSectionInfo();
            const level = this.submitUnderOpinionSubsection ? 4 : 3;
            this.previewHtml = "";
            this.previewWikitext = "";
            this.pendingNewSectionText = "";
            this.existingSectionText = "";
            this.sectionRevisionInfo = null;
            const draftFragment = this.getDraftFragment();
            if (!this.submitUnderOpinionSubsection) {
              const headers = draftFragment || this.buildHeadersForCriteria(level);
              if (!headers || !headers.trim()) {
                this.reportMissingOpinionEntries();
                return;
              }
              this.previewWikitext = headers;
              parseWikitextToHtml(headers, pageTitleToUse).then((html) => {
                this.previewHtml = html || "";
                if (this.previewHtml && this.currentStep === 2) {
                  this.triggerContentHooks("preview");
                }
              }).catch((e) => {
                console.error("[ReviewTool] parseWikitextToHtml failed", e);
                this.previewHtml = "";
              });
              return;
            }
            const opinionHeaderTitle = `${state_default.userName}${state_default.convByVar({ hant: " 的意見", hans: " 的意见" })}`;
            const h4s = draftFragment || this.buildOpinionContent(4);
            if (!h4s || !h4s.trim()) {
              this.reportMissingOpinionEntries(true);
              this.isSubmitting = false;
              return;
            }
            const fallbackFragment = h4s ? createHeaderMarkup(opinionHeaderTitle, 3) + h4s : "";
            const renderPreview = (fragment, existingText, newText) => {
              this.previewWikitext = fragment;
              this.existingSectionText = existingText;
              this.pendingNewSectionText = newText;
              parseWikitextToHtml(fragment, pageTitleToUse).then((html) => {
                this.previewHtml = html || "";
                if (this.previewHtml && this.currentStep === 2) {
                  this.triggerContentHooks("preview");
                }
              }).catch((e) => {
                console.error("[ReviewTool] parseWikitextToHtml failed", e);
                this.previewHtml = "";
              });
            };
            if (sectionIdToUse != null) {
              retrieveFullText(pageTitleToUse, sectionIdToUse).then(({ text, starttimestamp, basetimestamp }) => {
                this.sectionRevisionInfo = { starttimestamp, basetimestamp };
                const secText = text || "";
                const insertion = this.computeOpinionInsertion(secText, h4s, opinionHeaderTitle);
                renderPreview(insertion.previewFragment, secText, insertion.newSectionText);
              }).catch((e) => {
                console.error("[ReviewTool] retrieveFullText failed", e);
                this.sectionRevisionInfo = null;
                renderPreview(fallbackFragment, "", fallbackFragment);
              });
            } else {
              renderPreview(fallbackFragment, "", fallbackFragment);
            }
          },
          prepareDiffContent() {
            const { pageTitleToUse, sectionIdToUse } = this.getPendingReviewSectionInfo();
            this.diffHtml = "";
            const draftFragment = this.getDraftFragment();
            if (!this.submitUnderOpinionSubsection) {
              const headers = this.previewWikitext || draftFragment || this.buildHeadersForCriteria(3);
              if (!headers || !headers.trim()) {
                this.reportMissingOpinionEntries();
                return;
              }
              const runDiff = (oldText, newText) => {
                compareWikitext(oldText || "", newText).then((dhtml) => {
                  this.diffHtml = dhtml || "";
                  if (this.diffHtml && this.currentStep === 3) {
                    this.triggerContentHooks("diff");
                  }
                }).catch((e) => {
                  console.error("[ReviewTool] compareWikitext failed", e);
                  this.diffHtml = "";
                });
              };
              if (sectionIdToUse != null) {
                retrieveFullText(pageTitleToUse, sectionIdToUse).then(({ text, starttimestamp, basetimestamp }) => {
                  this.sectionRevisionInfo = { starttimestamp, basetimestamp };
                  const current = text || "";
                  this.existingSectionText = current;
                  runDiff(current, (current || "") + headers);
                }).catch((e) => {
                  console.error("[ReviewTool] retrieveFullText failed", e);
                  this.sectionRevisionInfo = null;
                  runDiff("", headers);
                });
              } else {
                runDiff("", headers);
              }
              return;
            }
            const opinionHeaderTitle = `${state_default.userName}${state_default.convByVar({ hant: " 的意見", hans: " 的意见" })}`;
            const h4s = draftFragment || this.buildOpinionContent(4);
            const runOpinionDiff = (oldText, newText) => {
              this.existingSectionText = oldText;
              this.pendingNewSectionText = newText;
              compareWikitext(oldText || "", newText).then((dhtml) => {
                this.diffHtml = dhtml || "";
                if (this.diffHtml && this.currentStep === 3) {
                  this.triggerContentHooks("diff");
                }
              }).catch((e) => {
                console.error("[ReviewTool] compareWikitext failed", e);
                this.diffHtml = "";
              });
            };
            if (this.pendingNewSectionText) {
              runOpinionDiff(this.existingSectionText || "", this.pendingNewSectionText);
              return;
            }
            const fallbackFragment = h4s ? createHeaderMarkup(opinionHeaderTitle, 3) + h4s : "";
            if (sectionIdToUse != null) {
              retrieveFullText(pageTitleToUse, sectionIdToUse).then(({ text, starttimestamp, basetimestamp }) => {
                this.sectionRevisionInfo = { starttimestamp, basetimestamp };
                const secText = text || "";
                const insertion = this.computeOpinionInsertion(secText, h4s, opinionHeaderTitle);
                runOpinionDiff(secText, insertion.newSectionText);
              }).catch((e) => {
                console.error("[ReviewTool] retrieveFullText failed", e);
                this.sectionRevisionInfo = null;
                runOpinionDiff("", fallbackFragment);
              });
            } else {
              runOpinionDiff("", fallbackFragment);
            }
          },
          computeOpinionInsertion(secText, h4s, opinionHeaderTitle) {
            if (!h4s || !h4s.trim()) {
              return { previewFragment: "", newSectionText: secText, insertedIntoExisting: false };
            }
            const h3LineRe = /^\s*(={3,})\s*(.*?)\s*\1\s*$/gm;
            const normalizeHeadingText = (s) => {
              if (!s) return "";
              let normalized = s.replace(/'''+/g, "").replace(/''/g, "");
              try {
                const txt = document.createElement("textarea");
                txt.innerHTML = normalized;
                normalized = txt.value;
              } catch (e) {
              }
              return normalized.replace(/\s+/g, " ").trim();
            };
            const targetNorm = normalizeHeadingText(opinionHeaderTitle);
            let match;
            while ((match = h3LineRe.exec(secText)) !== null) {
              const fullLine = match[0];
              const inner = match[2];
              if (normalizeHeadingText(inner) === targetNorm) {
                const headingLevel = match[1].length;
                const headingEnd = match.index + fullLine.length;
                const rest = secText.slice(headingEnd);
                const genericHeadingRe = /^\s*(={1,6})\s*([^\r\n]*?)\s*\1\s*$/gm;
                let insertPos = secText.length;
                let nextMatch;
                while ((nextMatch = genericHeadingRe.exec(rest)) !== null) {
                  const nextLevel = nextMatch[1].length;
                  if (nextLevel <= headingLevel) {
                    insertPos = headingEnd + nextMatch.index;
                    break;
                  }
                }
                const prefix = secText.slice(0, insertPos);
                const suffix = secText.slice(insertPos);
                const prefixEndsWithNewline = !prefix.length || /\r?\n$/.test(prefix);
                let normalized = h4s;
                if (prefixEndsWithNewline) {
                  normalized = normalized.replace(/^(?:\r?\n)+/, "");
                } else if (!/^\r?\n/.test(normalized)) {
                  normalized = "\n" + normalized;
                }
                if (!/\r?\n$/.test(normalized)) {
                  normalized += "\n";
                }
                if (suffix.length && !/^\r?\n/.test(suffix)) {
                  normalized += "\n";
                }
                const insertion = normalized;
                const newSectionText2 = prefix + insertion + secText.slice(insertPos);
                return { previewFragment: h4s, newSectionText: newSectionText2, insertedIntoExisting: true };
              }
            }
            const previewFragment = createHeaderMarkup(opinionHeaderTitle, 3) + h4s;
            const newSectionText = secText + previewFragment;
            return { previewFragment, newSectionText, insertedIntoExisting: false };
          },
          getStepClass(step) {
            return { "review-tool-multistep-dialog__stepper__step--active": step <= this.currentStep };
          },
          onPrimaryAction() {
            if (advanceDialogStep(this, {
              totalSteps: 4,
              onEnterEditStep: this.prepareEditDraft,
              onEnterPreviewStep: this.preparePreviewContent,
              onEnterDiffStep: this.prepareDiffContent,
              previewStepIndex: 2,
              diffStepIndex: 3
            })) {
              return;
            }
            this.submitReview();
          },
          onDefaultAction() {
            if (regressDialogStep(this)) {
              return;
            }
            this.closeDialog();
          },
          onUpdateOpen(newValue) {
            if (!newValue) {
              this.closeDialog();
            }
          },
          closeDialog() {
            this.open = false;
            setTimeout(() => {
              removeDialogMount();
            }, 300);
          },
          submitReview() {
            if (!this.selectedAssessmentType) {
              mw.notify(state_default.convByVar({ hant: "請選擇評審標準。", hans: "请选择评审标准。" }), {
                type: "error",
                title: "[ReviewTool]"
              });
              return;
            }
            this.isSubmitting = true;
            const payload = {
              articleTitle: state_default.articleTitle,
              userName: state_default.userName,
              submitUnderOpinionSubsection: !!this.submitUnderOpinionSubsection,
              assessmentType: this.selectedAssessmentType,
              selectedCriteria: Array.isArray(this.selectedCriteria) ? this.selectedCriteria.slice() : [],
              criterion: this.criterion
            };
            const headingEl = state_default.pendingReviewHeading || null;
            const sec = findSectionInfoFromHeading(headingEl);
            if (!sec || !sec.sectionId) {
              const msg = state_default.convByVar({ hant: "無法識別章節編號,請在討論頁的章節標題附近點擊「管理評審」。", hans: "无法识别章节编号,请在讨论页的章节标题附近点击“管理评审”。" });
              mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
              alert(msg);
              this.isSubmitting = false;
              return;
            }
            const level = this.submitUnderOpinionSubsection ? 4 : 3;
            const draftFragment = this.getDraftFragment();
            const buildHeadersForCriteria = (criteria, lvl) => {
              return criteria.map((c) => createHeaderMarkup(String(c), lvl)).join("");
            };
            const pageTitleToUse = sec.pageTitle || state_default.articleTitle || "";
            const sectionIdToUse = sec.sectionId;
            if (!this.submitUnderOpinionSubsection) {
              const headers = draftFragment || (Array.isArray(this.selectedCriteria) && this.selectedCriteria.length > 0 ? buildHeadersForCriteria(this.selectedCriteria, level) : this.criterion ? createHeaderMarkup(String(this.criterion), level) : "");
              if (!headers || !headers.trim()) {
                this.reportMissingOpinionEntries(true);
                this.isSubmitting = false;
                return;
              }
              appendTextToSection(pageTitleToUse, sectionIdToUse, headers, state_default.convByVar({ hant: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增評審項目", hans: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增评审项目" })).then((resp) => {
                mw && mw.notify && mw.notify(state_default.convByVar({ hant: "已成功新增評審項目。", hans: "已成功新增评审项目。" }), { tag: "review-tool" });
                this.isSubmitting = false;
                this.open = false;
                state_default.pendingReviewHeading = null;
                setTimeout(() => {
                  removeDialogMount();
                }, 200);
              }).catch((err) => {
                console.error("[ReviewTool] appendTextToSection failed", err);
                const msg = state_default.convByVar({ hant: "新增評審項目失敗,請稍後再試。", hans: "新增评审项目失败,请稍后再试。" });
                mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
                alert(msg);
                this.isSubmitting = false;
              });
              return;
            }
            const opinionHeaderTitle = `${state_default.userName}${state_default.convByVar({ hant: " 的意見", hans: " 的意见" })}`;
            const h4s = draftFragment || this.buildOpinionContent(4);
            const revisionInfo = this.sectionRevisionInfo;
            if (!revisionInfo) {
              const msg = state_default.convByVar({ hant: "請先預覽並檢視差異,以取得最新段落資訊後再提交。", hans: "请先预览并查看差异,以取得最新段落资讯后再提交。" });
              mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
              alert(msg);
              this.isSubmitting = false;
              return;
            }
            const secWikitext = typeof this.existingSectionText === "string" ? this.existingSectionText : "";
            const insertion = this.computeOpinionInsertion(secWikitext, h4s, opinionHeaderTitle);
            if (insertion.insertedIntoExisting) {
              replaceSectionText(
                pageTitleToUse,
                sectionIdToUse,
                insertion.newSectionText,
                state_default.convByVar({ hant: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增評審子項", hans: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增评审子项" }),
                revisionInfo
              ).then((resp) => {
                mw && mw.notify && mw.notify(state_default.convByVar({ hant: "已成功新增評審子項。", hans: "已成功新增评审子项。" }), { tag: "review-tool" });
                this.isSubmitting = false;
                this.open = false;
                state_default.pendingReviewHeading = null;
                setTimeout(() => {
                  removeDialogMount();
                }, 200);
              }).catch((err) => {
                console.error("[ReviewTool] replaceSectionText failed", err);
                const msg = state_default.convByVar({ hant: "新增評審子項失敗,請稍後再試。", hans: "新增评审子项失败,请稍后再试。" });
                mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
                alert(msg);
                this.isSubmitting = false;
              });
            } else {
              appendTextToSection(
                pageTitleToUse,
                sectionIdToUse,
                insertion.previewFragment,
                state_default.convByVar({ hant: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增評審項", hans: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增评审项目" })
              ).then((resp) => {
                mw && mw.notify && mw.notify(state_default.convByVar({ hant: "已成功新增評審項目。", hans: "已成功新增评审项目。" }), { tag: "review-tool" });
                this.isSubmitting = false;
                this.open = false;
                state_default.pendingReviewHeading = null;
                setTimeout(() => {
                  removeDialogMount();
                }, 200);
              }).catch((err) => {
                console.error("[ReviewTool] appendTextToSection failed", err);
                const msg = state_default.convByVar({ hant: "新增評審項目失敗,請稍後再試。", hans: "新增评审项目失败,请稍后再试。" });
                mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
                alert(msg);
                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="review-tool-dialog review-tool-review-management-dialog review-tool-multistep-dialog"
                		>

                <template #header>
                    <div class="review-tool-multistep-dialog__header-top">
                        <h2>{{ $options.i18n.dialogTitle }}</h2>
                    </div>

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

                <!-- Step 0: Form -->
                <div v-if="currentStep === 0">
                    <div class="review-tool-form-section">
                        <cdx-checkbox v-model="submitUnderOpinionSubsection">{{ $options.i18n.submitUnderOpinionSubsection }}</cdx-checkbox>
                    </div>

                    <div class="review-tool-form-section">
                        <label class="review-tool-select-label">{{ $options.i18n.selectCriterion }}</label>
                        <cdx-select
                            v-model:selected="selectedAssessmentType"
                            :menu-items="codexOptions"
                            :default-label="$options.i18n.criterionPlaceholder"
                        ></cdx-select>
                    </div>

                    <div class="review-tool-form-section" v-if="selectedAssessmentType">
                        <label class="review-tool-checkbox-label">{{ $options.i18n.addCriteriaToReview }}</label>
                        <div class="review-tool-suggested-criteria-grid">
                            <cdx-checkbox
                                v-for="(item, idx) in codexCriteriaItems"
                                :key="idx"
                                v-model="selectedCriteria"
                                :input-value="item.value"
                            >
                                {{ item.label }}
                            </cdx-checkbox>
                        </div>
                    </div>
                </div>

                <!-- Step 1: Edit Draft -->
                <div v-else-if="currentStep === 1" class="review-tool-edit-step">
                    <h3>{{ $options.i18n.editHeading }}</h3>
                    <p class="review-tool-edit-step__instruction">{{ $options.i18n.editInstruction }}</p>
                    <cdx-text-area
                        v-model="editedDraft"
                        :placeholder="$options.i18n.editPlaceholder"
                        rows="16"
                    ></cdx-text-area>
                </div>

                <!-- Step 2: Preview -->
                <div v-else-if="currentStep === 2" class="review-tool-preview">
                    <h3>{{ $options.i18n.previewHeading }}</h3>
                    <div
                        v-if="previewHtml"
                        class="review-tool-preview-pre review-tool-preview-pre--html"
                        ref="previewHtmlHost"
                        v-html="previewHtml"
                    ></div>
                    <pre class="review-tool-preview-pre" v-else>{{ previewWikitext || editedDraft || (selectedCriteria.length ? selectedCriteria.join('\\n') : (criterion || $options.i18n.noSpecificCriteria)) }}</pre>
                </div>

                <!-- Step 3: Diff & Save -->
                <div v-else-if="currentStep === 3" class="review-tool-diff">
                    <h3>{{ $options.i18n.diffHeading }}</h3>
                    <div
                        v-if="diffHtml"
                        class="review-tool-diff-pre review-tool-diff-pre--html"
                        ref="diffHtmlHost"
                        v-html="diffHtml"
                    ></div>
                    <div v-else>
                        <p>{{ $options.i18n.noDiff }}</p>
                    </div>
                </div>

            </cdx-dialog>
            `
      });
      registerCodexComponents(app, Codex);
      mountApp(app);
    }).catch((error) => {
      console.error("[ReviewTool] 無法加載 Codex:", error);
      mw.notify(state_default.convByVar({ hant: "無法加載對話框組件。", hans: "无法加载对话框组件。" }), {
        type: "error",
        title: "[ReviewTool]"
      });
    });
  }
  function openReviewManagementDialog() {
    if (getMountedApp && getMountedApp()) removeDialogMount();
    createReviewManagementDialog();
  }

  // src/dialogs/check_writing.ts
  init_state();

  // src/annotations.ts
  init_state();
  var KEY_PREFIX = "reviewtool:annotations:";
  function storageKeyForPage(pageName) {
    return KEY_PREFIX + (pageName || "unknown");
  }
  function getStorage(type) {
    if (typeof window === "undefined") return null;
    try {
      return type === "local" ? window.localStorage : window.sessionStorage;
    } catch (e) {
      console.error(`[ReviewTool] ${type}Storage unavailable`, e);
      return null;
    }
  }
  function createEmptyStore(pageName) {
    return {
      pageName,
      createdAt: Date.now(),
      annotations: []
    };
  }
  function uuidv4() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
      const r = Math.random() * 16 | 0;
      const v = c === "x" ? r : r & 3 | 8;
      return v.toString(16);
    });
  }
  function loadAnnotations(pageName) {
    const key = storageKeyForPage(pageName);
    const localStore = getStorage("local");
    const sessionStore = getStorage("session");
    let raw = null;
    let source = null;
    if (localStore) {
      try {
        raw = localStore.getItem(key);
        if (raw) source = "local";
      } catch (e) {
        console.error("[ReviewTool] failed to read annotations from localStorage", e);
      }
    }
    if (!raw && sessionStore) {
      try {
        raw = sessionStore.getItem(key);
        if (raw) source = "session";
      } catch (e) {
        console.error("[ReviewTool] failed to read annotations from sessionStorage", e);
      }
    }
    if (!raw) {
      return createEmptyStore(pageName);
    }
    let parsed = null;
    try {
      parsed = JSON.parse(raw);
    } catch (e) {
      console.warn("[ReviewTool] failed to parse annotations payload", e);
      return createEmptyStore(pageName);
    }
    const annotations = Array.isArray(parsed == null ? void 0 : parsed.annotations) ? parsed.annotations.map((anno) => __spreadProps(__spreadValues({}, anno), {
      sentencePos: typeof (anno == null ? void 0 : anno.sentencePos) === "string" ? anno.sentencePos : ""
    })) : [];
    const normalized = {
      pageName: (parsed == null ? void 0 : parsed.pageName) || pageName,
      createdAt: (parsed == null ? void 0 : parsed.createdAt) || Date.now(),
      annotations
    };
    if (source === "session" && localStore) {
      try {
        localStore.setItem(key, JSON.stringify(normalized));
        sessionStore == null ? void 0 : sessionStore.removeItem(key);
      } catch (e) {
        console.error("[ReviewTool] failed to migrate annotations from sessionStorage to localStorage", e);
      }
    }
    return normalized;
  }
  function saveAnnotations(store) {
    const key = storageKeyForPage(store.pageName);
    const payload = JSON.stringify(store);
    const localStore = getStorage("local");
    if (localStore) {
      try {
        localStore.setItem(key, payload);
        return;
      } catch (e) {
        console.error("[ReviewTool] failed to save annotations to localStorage", e);
      }
    }
    const sessionStore = getStorage("session");
    if (sessionStore) {
      try {
        sessionStore.setItem(key, payload);
      } catch (e) {
        console.error("[ReviewTool] failed to save annotations to sessionStorage fallback", e);
      }
    } else {
      console.error("[ReviewTool] no available storage to save annotations");
    }
  }
  function createAnnotation(pageName, sectionPath, sentenceText, opinion, sentencePos = "") {
    const store = loadAnnotations(pageName);
    const normalizedSectionPath = sectionPath === "目次" ? "序言" : sectionPath;
    const anno = {
      id: uuidv4(),
      sectionPath: normalizedSectionPath,
      sentencePos,
      sentenceText,
      opinion,
      createdBy: state_default.userName || "unknown",
      createdAt: Date.now(),
      resolved: false
    };
    store.annotations.push(anno);
    saveAnnotations(store);
    return anno;
  }
  function getAnnotation(pageName, id) {
    const store = loadAnnotations(pageName);
    return store.annotations.find((a) => a.id === id) || null;
  }
  function updateAnnotation(pageName, id, updates) {
    const store = loadAnnotations(pageName);
    const idx = store.annotations.findIndex((a) => a.id === id);
    if (idx === -1) return null;
    const updated = __spreadValues(__spreadValues({}, store.annotations[idx]), updates);
    store.annotations[idx] = updated;
    saveAnnotations(store);
    return updated;
  }
  function deleteAnnotation(pageName, id) {
    const store = loadAnnotations(pageName);
    const before = store.annotations.length;
    store.annotations = store.annotations.filter((a) => a.id !== id);
    if (store.annotations.length !== before) {
      saveAnnotations(store);
      return true;
    }
    return false;
  }
  function clearAnnotations(pageName) {
    const key = storageKeyForPage(pageName);
    const localStore = getStorage("local");
    const sessionStore = getStorage("session");
    let removed = false;
    if (localStore) {
      try {
        if (localStore.getItem(key) !== null) removed = true;
        localStore.removeItem(key);
      } catch (e) {
        console.error("[ReviewTool] failed to clear annotations from localStorage", e);
      }
    }
    if (sessionStore) {
      try {
        if (sessionStore.getItem(key) !== null) removed = true;
        sessionStore.removeItem(key);
      } catch (e) {
        console.error("[ReviewTool] failed to clear annotations from sessionStorage", e);
      }
    }
    return removed;
  }
  function sortAnnotationsByTimestamp(list) {
    return [...list].sort((a, b) => a.createdAt - b.createdAt);
  }
  function buildAnnotationGroups(pageName) {
    const store = loadAnnotations(pageName);
    if (!store.annotations.length) {
      return [];
    }
    const buckets = /* @__PURE__ */ new Map();
    for (const anno of store.annotations) {
      const key = typeof anno.sectionPath === "string" && anno.sectionPath.trim() ? anno.sectionPath.trim() : "";
      const existing = buckets.get(key);
      if (existing) {
        existing.push(anno);
      } else {
        buckets.set(key, [anno]);
      }
    }
    const groups = [];
    for (const [sectionPath, annotations] of buckets.entries()) {
      groups.push({
        sectionPath,
        annotations: sortAnnotationsByTimestamp(annotations)
      });
    }
    groups.sort((a, b) => {
      var _a, _b, _c, _d;
      const aTs = (_b = (_a = a.annotations[0]) == null ? void 0 : _a.createdAt) != null ? _b : Number.MAX_SAFE_INTEGER;
      const bTs = (_d = (_c = b.annotations[0]) == null ? void 0 : _c.createdAt) != null ? _d : Number.MAX_SAFE_INTEGER;
      if (aTs === bTs) {
        return a.sectionPath.localeCompare(b.sectionPath);
      }
      return aTs - bTs;
    });
    return groups;
  }

  // src/dom/numeric_pos.ts
  var DEFAULT_ROOT_SELECTOR = "#mw-content-text";
  var DEFAULT_PADDING = 6;
  function resolveRoot(root) {
    if (!root) {
      return document.querySelector(DEFAULT_ROOT_SELECTOR);
    }
    if (typeof root === "string") {
      return document.querySelector(root);
    }
    return root;
  }
  function countPreviousElementSiblings(node) {
    let index = 0;
    let sibling = node ? node.previousElementSibling : null;
    while (sibling) {
      index++;
      sibling = sibling.previousElementSibling;
    }
    return index;
  }
  function getElementPathArray(element, root) {
    if (!element) return null;
    const rootEl = resolveRoot(root);
    if (!rootEl || !rootEl.contains(element)) return null;
    const path = [];
    let node = element;
    while (node && node !== rootEl) {
      path.push(countPreviousElementSiblings(node));
      node = node.parentElement;
    }
    if (node !== rootEl) {
      return null;
    }
    path.reverse();
    return path;
  }
  function pathArrayToKey(path, paddingWidth = DEFAULT_PADDING) {
    if (!path) return null;
    const width = Math.max(1, paddingWidth | 0);
    return path.map((segment) => String(segment).padStart(width, "0")).join(".");
  }
  function getElementOrderKey(element, options) {
    var _a, _b;
    const path = getElementPathArray(element, (_a = options == null ? void 0 : options.root) != null ? _a : DEFAULT_ROOT_SELECTOR);
    return pathArrayToKey(path, (_b = options == null ? void 0 : options.paddingWidth) != null ? _b : DEFAULT_PADDING);
  }
  function compareOrderKeys(a, b) {
    if (!a && !b) return 0;
    if (!a) return -1;
    if (!b) return 1;
    const partsA = a.split(".").map((part) => parseInt(part, 10));
    const partsB = b.split(".").map((part) => parseInt(part, 10));
    const len = Math.min(partsA.length, partsB.length);
    for (let i = 0; i < len; i++) {
      if (partsA[i] !== partsB[i]) {
        return partsA[i] - partsB[i];
      }
    }
    return partsA.length - partsB.length;
  }

  // src/dialogs/check_writing.ts
  function createCheckWritingDialog() {
    loadCodexAndVue().then(({ Vue, Codex }) => {
      const app = Vue.createMwApp({
        i18n: {
          dialogTitle: state_default.convByVar({
            hant: "檢查「",
            hans: "检查「"
          }) + state_default.articleTitle + state_default.convByVar({ hant: "」的文筆", hans: "」的文笔" }),
          save: state_default.convByVar({ hant: "儲存", hans: "保存" }),
          saving: state_default.convByVar({ hant: "儲存中…", hans: "保存中…" }),
          cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
          addChapter: state_default.convByVar({ hant: "新增章節", hans: "新增章节" }),
          removeChapter: state_default.convByVar({ hant: "刪除章節", hans: "删除章节" }),
          addSuggestion: state_default.convByVar({ hant: "新增意見", hans: "新增意见" }),
          removeSuggestion: state_default.convByVar({ hant: "刪除意見", hans: "删除意见" }),
          chapterTitleLabel: state_default.convByVar({ hant: "章節標題", hans: "章节标题" }),
          quoteLabel: state_default.convByVar({ hant: "引用原文", hans: "引用原文" }),
          quotePlaceholder: state_default.convByVar({ hant: "原文句子(可留空)", hans: "原文句子(可留空)" }),
          suggestionPlaceholder: state_default.convByVar({ hant: "意見或建議", hans: "意见或建议" }),
          next: state_default.convByVar({ hant: "下一步", hans: "下一步" }),
          previous: state_default.convByVar({ hant: "上一步", hans: "上一步" }),
          previewHeading: state_default.convByVar({ hant: "預覽", hans: "预览" }),
          diffHeading: state_default.convByVar({ hant: "差異", hans: "差异" }),
          diffLoading: state_default.convByVar({ hant: "差異載入中…", hans: "差异载入中…" }),
          editHeading: state_default.convByVar({ hant: "編輯建議", hans: "编辑建议" }),
          editInstruction: state_default.convByVar({ hant: "在此調整要新增的維基語法內容,再前往預覽或差異。", hans: "在此调整要新增的维基语法内容,再前往预览或差异。" }),
          editPlaceholder: state_default.convByVar({ hant: "在此輸入或修改文筆建議的維基語法內容…", hans: "在此输入或修改文笔建议的维基语法内容…" }),
          loadAnnotations: state_default.convByVar({ hant: "載入批註", hans: "载入批注" }),
          importFromFile: state_default.convByVar({ hant: "從檔案載入", hans: "从文件载入" }),
          importSuccess: state_default.convByVar({ hant: "已從檔案載入批註。", hans: "已从文件载入批注。" }),
          importError: state_default.convByVar({ hant: "載入檔案時發生錯誤。", hans: "读取文件时发生错误。" }),
          importInvalid: state_default.convByVar({ hant: "無效的批註檔案。", hans: "无效的批注文件。" }),
          annotationFallbackChapter: state_default.convByVar({ hant: "(未指定章節)", hans: "(未指定章节)" })
        },
        data() {
          return {
            open: true,
            isSaving: false,
            isLoadingAnnotations: false,
            currentStep: 0,
            chapters: [
              { title: "", suggestions: [{ quote: "", suggestion: "" }] }
            ],
            previewWikitext: "",
            previewHtml: "",
            existingSectionText: "",
            pendingNewSectionText: "",
            diffHtml: "",
            diffLines: [],
            editedDraft: ""
            // no persistent data needed for import UI; the file input is handled via ref
          };
        },
        computed: {
          primaryAction() {
            if (this.currentStep < 3) {
              return { label: this.$options.i18n.next || "Next", actionType: "progressive", disabled: false };
            }
            return { label: this.isSaving ? this.$options.i18n.saving : this.$options.i18n.save, actionType: "progressive", disabled: this.isSaving };
          },
          defaultAction() {
            if (this.currentStep > 0) return { label: this.$options.i18n.previous || "Previous", disabled: false };
            return { label: this.$options.i18n.cancel, disabled: false };
          },
          showAnnotationLoaderButton() {
            return this.currentStep === 0;
          }
        },
        methods: {
          triggerContentHooks(kind) {
            triggerDialogContentHooks(this, kind);
          },
          getPendingCheckWritingSectionInfo() {
            const headingEl = state_default.pendingReviewHeading || null;
            const sec = findSectionInfoFromHeading(headingEl);
            const pageTitleToUse = sec && sec.pageTitle ? sec.pageTitle : state_default.articleTitle || "";
            const sectionIdToUse = typeof (sec && sec.sectionId) === "number" ? sec.sectionId : sec && sec.sectionId != null ? sec.sectionId : null;
            return { headingEl, sec, pageTitleToUse, sectionIdToUse };
          },
          getStepClass(step) {
            return { "review-tool-multistep-dialog__stepper__step--active": step <= this.currentStep };
          },
          prepareEditDraft() {
            this.editedDraft = this.buildWikitext().trim();
          },
          preparePreviewContent() {
            const { pageTitleToUse, sectionIdToUse } = this.getPendingCheckWritingSectionInfo();
            this.previewHtml = "";
            this.previewWikitext = "";
            this.pendingNewSectionText = "";
            this.existingSectionText = "";
            this.diffHtml = "";
            this.diffLines = [];
            const bundle = this.buildPreviewBundle();
            if (!bundle) {
              return;
            }
            const { previewFragment, appendSuffix } = bundle;
            this.previewWikitext = previewFragment;
            const renderPreview = (existingText) => {
              const baseline = existingText || "";
              this.existingSectionText = baseline;
              this.pendingNewSectionText = baseline + appendSuffix;
              parseWikitextToHtml(previewFragment, pageTitleToUse).then((html) => {
                this.previewHtml = html || "";
                if (this.previewHtml && this.currentStep === 2) {
                  this.triggerContentHooks("preview");
                }
              }).catch((e) => {
                console.error("[ReviewTool] parseWikitextToHtml failed", e);
                this.previewHtml = "";
              });
            };
            if (sectionIdToUse != null) {
              retrieveFullText(pageTitleToUse, sectionIdToUse).then(({ text }) => {
                renderPreview(text || "");
              }).catch((err) => {
                console.error("[ReviewTool] retrieveFullText failed", err);
                renderPreview("");
              });
            } else {
              renderPreview("");
            }
          },
          prepareDiffContent() {
            const { pageTitleToUse, sectionIdToUse } = this.getPendingCheckWritingSectionInfo();
            this.diffHtml = "";
            this.diffLines = [];
            const bundle = this.buildPreviewBundle();
            if (!bundle) {
              return;
            }
            const { previewFragment, appendSuffix } = bundle;
            this.previewWikitext = previewFragment;
            const runDiff = (existingText) => {
              const baseline = existingText || "";
              const newSectionText = baseline + appendSuffix;
              this.existingSectionText = baseline;
              this.pendingNewSectionText = newSectionText;
              compareWikitext(baseline, newSectionText).then((diffHtml) => {
                this.diffHtml = diffHtml || "";
                if (this.diffHtml && this.currentStep === 3) {
                  this.triggerContentHooks("diff");
                } else {
                  this.diffLines = this.buildDiffLines(baseline, appendSuffix);
                }
              }).catch((err) => {
                console.error("[ReviewTool] compareWikitext failed", err);
                this.diffHtml = "";
                this.diffLines = this.buildDiffLines(baseline, appendSuffix);
              });
            };
            if (this.pendingNewSectionText && typeof this.existingSectionText === "string" && this.pendingNewSectionText === this.existingSectionText + appendSuffix) {
              runDiff(this.existingSectionText);
              return;
            }
            if (sectionIdToUse != null) {
              retrieveFullText(pageTitleToUse, sectionIdToUse).then(({ text }) => {
                runDiff(text || "");
              }).catch((err) => {
                console.error("[ReviewTool] retrieveFullText failed", err);
                runDiff("");
              });
            } else {
              runDiff("");
            }
          },
          onPrimaryAction() {
            if (advanceDialogStep(this, {
              totalSteps: 4,
              onEnterEditStep: this.prepareEditDraft,
              onEnterPreviewStep: this.preparePreviewContent,
              onEnterDiffStep: this.prepareDiffContent,
              previewStepIndex: 2,
              diffStepIndex: 3
            })) {
              return;
            }
            this.saveCheckWriting();
          },
          onDefaultAction() {
            if (regressDialogStep(this)) {
              return;
            }
            this.closeDialog();
          },
          onUpdateOpen(newValue) {
            if (!newValue) {
              this.closeDialog();
            }
          },
          closeDialog() {
            this.open = false;
            setTimeout(() => {
              removeDialogMount();
            }, 300);
          },
          buildPreviewBundle() {
            const draft = (this.editedDraft || "").trim();
            const fragment = draft || this.buildWikitext().trim();
            if (!fragment) {
              return null;
            }
            const appendSuffix = `

${fragment}`;
            return { previewFragment: fragment, appendSuffix };
          },
          buildWikitext() {
            let wikitext = "";
            for (const ch of this.chapters) {
              const title = (ch.title || "").trim();
              wikitext += "'''" + title + "'''\n";
              for (const s of ch.suggestions || []) {
                const quote = (s.quote || "").trim();
                const suggestion = (s.suggestion || "").trim().replace(/\n{2,}/g, "{{pb}}").replace(/\n/g, "<br>");
                wikitext += `* {{rvw|1=${quote}}} —— ${suggestion}
`;
              }
              wikitext += "--~~~~\n\n";
            }
            wikitext = wikitext.replace("{{rvw|1=}} —— ", "");
            return wikitext;
          },
          buildDiffLines(oldText, appendedFragment) {
            const oldLines = (oldText || "").split(/\r?\n/);
            const appendedOnly = (appendedFragment || "").replace(/^\s*\n+/, "");
            const newLines = appendedOnly.split(/\r?\n/);
            const out = [];
            out.push("--- Existing section ---");
            out.push(...oldLines.map((l) => "  " + l));
            out.push("");
            out.push("+++ New content to append +++");
            out.push(...newLines.map((l) => "+ " + l));
            return out;
          },
          // Import helpers
          handleImportClick() {
            const input = this.$refs && this.$refs.annotationImportInput || null;
            if (input && typeof input.click === "function") {
              input.value = "";
              input.click();
            } else {
              console.warn("[ReviewTool] file input not available for import");
            }
          },
          generateImportAnnotationId() {
            return `import-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
          },
          normalizeImportedAnnotation(raw, fallbackSection = "") {
            const id = typeof (raw == null ? void 0 : raw.id) === "string" && raw.id.trim() ? raw.id.trim() : this.generateImportAnnotationId();
            const sectionPath = typeof (raw == null ? void 0 : raw.sectionPath) === "string" && raw.sectionPath.trim() ? raw.sectionPath.trim() : fallbackSection || "";
            return {
              id,
              sectionPath,
              sentencePos: typeof (raw == null ? void 0 : raw.sentencePos) === "string" ? raw.sentencePos : "",
              sentenceText: (raw == null ? void 0 : raw.sentenceText) || (raw == null ? void 0 : raw.quote) || "",
              opinion: (raw == null ? void 0 : raw.opinion) || (raw == null ? void 0 : raw.suggestion) || "",
              createdBy: (raw == null ? void 0 : raw.createdBy) || state_default.userName || "import",
              createdAt: typeof (raw == null ? void 0 : raw.createdAt) === "number" ? raw.createdAt : Date.now(),
              resolved: Boolean(raw == null ? void 0 : raw.resolved)
            };
          },
          onAnnotationFileSelected(ev) {
            const input = ev && ev.target;
            if (!input || !input.files || !input.files.length) return;
            const file = input.files[0];
            const reader = new FileReader();
            reader.onload = (e) => {
              try {
                const text = e.target && e.target.result || "";
                const parsed = JSON.parse(text);
                const pageName = state_default.articleTitle || "";
                if (!pageName) {
                  this.reportAnnotationLoadFailure(state_default.convByVar({ hant: "無法識別條目名稱,無法載入檔案中的批註。", hans: "无法识别条目名称,无法载入文件中的批注。" }));
                  return;
                }
                const importedAnnotations = [];
                if (Array.isArray(parsed.annotations)) {
                  for (const a of parsed.annotations) {
                    importedAnnotations.push(this.normalizeImportedAnnotation(a));
                  }
                } else if (Array.isArray(parsed.groups)) {
                  for (const g of parsed.groups) {
                    const section = g.sectionPath || "";
                    const annos = Array.isArray(g.annotations) ? g.annotations : [];
                    for (const a of annos) {
                      importedAnnotations.push(this.normalizeImportedAnnotation(__spreadProps(__spreadValues({}, a), { sectionPath: a.sectionPath || section }), section));
                    }
                  }
                } else {
                  throw new Error("invalid-format");
                }
                if (!importedAnnotations.length) {
                  throw new Error("empty-import");
                }
                this.applyImportedAnnotations(importedAnnotations);
                const msg = this.$options.i18n.importSuccess || "Imported annotations from file.";
                mw && mw.notify && mw.notify(msg, { tag: "review-tool" });
              } catch (err) {
                console.error("[ReviewTool] failed to import annotations from file", err);
                const msg = this.$options.i18n.importInvalid || "Invalid annotation file.";
                this.reportAnnotationLoadFailure(msg);
              }
            };
            reader.onerror = (err) => {
              console.error("[ReviewTool] FileReader error", err);
              const msg = this.$options.i18n.importError || "Failed to read file.";
              this.reportAnnotationLoadFailure(msg);
            };
            reader.readAsText(file, "utf-8");
          },
          loadAnnotationsIntoForm() {
            if (this.isLoadingAnnotations) {
              return;
            }
            this.isLoadingAnnotations = true;
            try {
              const pageName = state_default.articleTitle || "";
              if (!pageName) {
                this.reportAnnotationLoadFailure(state_default.convByVar({ hant: "無法識別條目名稱,無法載入批註。", hans: "无法识别条目名称,无法载入批注。" }));
                return;
              }
              const groups = buildAnnotationGroups(pageName);
              if (!groups.length) {
                this.reportAnnotationLoadFailure(state_default.convByVar({ hant: "目前沒有可載入的批註。", hans: "目前没有可载入的批注。" }));
                return;
              }
              const nextChapters = this.buildChaptersFromAnnotationGroups(groups);
              if (!nextChapters.length) {
                this.reportAnnotationLoadFailure(state_default.convByVar({ hant: "批註內容為空,請稍後再試。", hans: "批注内容为空,请稍后再试。" }));
                return;
              }
              this.applyAnnotationChapters(nextChapters);
              const successMsg = state_default.convByVar({ hant: "已將批註載入表單,請檢查後繼續。", hans: "已将批注载入表单,请检查后继续。" });
              mw && mw.notify && mw.notify(successMsg, { tag: "review-tool" });
            } finally {
              this.isLoadingAnnotations = false;
            }
          },
          buildChaptersFromAnnotationGroups(groups) {
            if (!groups.length) {
              return [];
            }
            const fallbackTitle = this.$options.i18n.annotationFallbackChapter || "";
            const sortedGroups = groups.map((group) => __spreadProps(__spreadValues({}, group), {
              annotations: this.sortAnnotationsByPosition(group.annotations)
            })).sort((a, b) => {
              const firstA = a.annotations[0];
              const firstB = b.annotations[0];
              const cmp = compareOrderKeys(firstA == null ? void 0 : firstA.sentencePos, firstB == null ? void 0 : firstB.sentencePos);
              if (cmp !== 0) return cmp;
              return (a.sectionPath || "").localeCompare(b.sectionPath || "");
            });
            const mapped = sortedGroups.map((group) => {
              const suggestions = (group.annotations || []).map((anno) => ({
                quote: anno.sentenceText || "",
                suggestion: anno.opinion || ""
              }));
              const usableSuggestions = suggestions.length ? suggestions : [{ quote: "", suggestion: "" }];
              return {
                title: group.sectionPath || fallbackTitle,
                suggestions: usableSuggestions
              };
            }).filter((group) => Array.isArray(group.suggestions) && group.suggestions.length);
            return mapped;
          },
          applyAnnotationChapters(nextChapters) {
            if (!nextChapters.length) {
              return;
            }
            this.chapters = nextChapters;
            if (this.currentStep === 2) {
              this.preparePreviewContent();
            } else if (this.currentStep === 3) {
              this.prepareDiffContent();
            }
          },
          sortAnnotationsByPosition(list) {
            if (!Array.isArray(list)) return [];
            return list.slice().sort((a, b) => {
              const cmp = compareOrderKeys(a == null ? void 0 : a.sentencePos, b == null ? void 0 : b.sentencePos);
              if (cmp !== 0) return cmp;
              return (a.createdAt || 0) - (b.createdAt || 0);
            });
          },
          groupAnnotationsBySection(list) {
            const buckets = /* @__PURE__ */ new Map();
            list.forEach((anno) => {
              const key = (anno.sectionPath || "").trim();
              if (!buckets.has(key)) buckets.set(key, []);
              buckets.get(key).push(anno);
            });
            return Array.from(buckets.entries()).map(([sectionPath, annotations]) => ({
              sectionPath,
              annotations
            }));
          },
          applyImportedAnnotations(importedAnnotations) {
            if (!Array.isArray(importedAnnotations) || !importedAnnotations.length) {
              throw new Error("empty-import");
            }
            const groups = this.groupAnnotationsBySection(importedAnnotations);
            const chapters = this.buildChaptersFromAnnotationGroups(groups);
            if (!chapters.length) {
              throw new Error("empty-chapters");
            }
            this.applyAnnotationChapters(chapters);
          },
          reportAnnotationLoadFailure(message) {
            mw && mw.notify && mw.notify(message, { type: "warn", title: "[ReviewTool]" });
            alert(message);
          },
          saveCheckWriting() {
            this.isSaving = true;
            const { sec, pageTitleToUse, sectionIdToUse } = this.getPendingCheckWritingSectionInfo();
            if (!sec || sectionIdToUse == null) {
              const msg = state_default.convByVar({ hant: "無法識別文筆章節編號,請在討論頁的文筆章節附近點擊「檢查文筆」。", hans: "无法识别文笔章节编号,请在讨论页的文笔章节附近点击“检查文笔”。" });
              mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
              alert(msg);
              this.isSaving = false;
              return;
            }
            const bundle = this.buildPreviewBundle();
            if (!bundle) {
              const msg = state_default.convByVar({ hant: "請先輸入文筆建議內容,再嘗試儲存。", hans: "请先输入文笔建议内容,再尝试保存。" });
              mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
              alert(msg);
              this.isSaving = false;
              return;
            }
            appendTextToSection(
              pageTitleToUse,
              sectionIdToUse,
              bundle.appendSuffix,
              state_default.convByVar({ hant: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增文筆建議", hans: "使用 [[User:SuperGrey/gadgets/ReviewTool|ReviewTool]] 新增文笔建议" })
            ).then((resp) => {
              mw && mw.notify && mw.notify(state_default.convByVar({ hant: "已成功新增文筆建議。", hans: "已成功新增文笔建议。" }), { tag: "review-tool" });
              this.isSaving = false;
              this.open = false;
              state_default.pendingReviewHeading = null;
              setTimeout(() => {
                removeDialogMount();
              }, 200);
            }).catch((err) => {
              console.error("[ReviewTool] appendTextToSection failed", err);
              const msg = state_default.convByVar({ hant: "新增文筆建議失敗,請稍後再試。", hans: "新增文笔建议失败,请稍后再试。" });
              mw && mw.notify && mw.notify(msg, { type: "error", title: "[ReviewTool]" });
              alert(msg);
              this.isSaving = false;
            });
          },
          addChapter() {
            this.chapters.push({ title: "", suggestions: [{ quote: "", suggestion: "" }] });
          },
          removeChapter(idx) {
            if (this.chapters.length <= 1) {
              return;
            }
            this.chapters.splice(idx, 1);
          },
          addSuggestion(chIdx) {
            this.chapters[chIdx].suggestions.push({ quote: "", suggestion: "" });
          },
          removeSuggestion(chIdx, sIdx) {
            const suggestions = this.chapters[chIdx].suggestions;
            if (suggestions.length <= 1) {
              return;
            }
            suggestions.splice(sIdx, 1);
          }
        },
        template: `
            <cdx-dialog
				v-model:open="open"
                :title="$options.i18n.dialogTitle"
				:use-close-button="true"
				@update:open="onUpdateOpen"
                class="review-tool-dialog review-tool-check-writing-dialog review-tool-multistep-dialog"
			>

				<template #header>
                    <div class="review-tool-multistep-dialog__header-top">
                        <h2>{{ $options.i18n.dialogTitle }}</h2>
                    </div>

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

				<!-- Step 0: Form -->
                <div v-if="currentStep === 0">
					<div v-for="(ch, chIdx) in chapters" :key="chIdx" class="review-tool-form-section chapter-block">
						<cdx-text-input v-model="ch.title" :placeholder="$options.i18n.chapterTitleLabel" class="chapter-title-input"></cdx-text-input>

                        <div class="chapter-suggestions">
                            <div v-for="(s, sIdx) in ch.suggestions" :key="sIdx" class="suggestion-row">
                                <div class="suggestion-bullet" aria-hidden="true"></div>
                                <div class="suggestion-columns">
                                    <div class="quote-col">
                                        <cdx-text-area class="quote-area" v-model="s.quote" :placeholder="$options.i18n.quotePlaceholder" rows="1"></cdx-text-area>
                                    </div>
                                    <div class="suggestion-col">
                                        <cdx-text-area class="suggestion-area" v-model="s.suggestion" :placeholder="$options.i18n.suggestionPlaceholder" rows="1"></cdx-text-area>
                                    </div>
                                    <div class="suggestion-controls">
                                        <cdx-button size="small" class="cdx-button--icon-only" :aria-label="$options.i18n.removeSuggestion" :disabled="ch.suggestions.length <= 1" @click.prevent="removeSuggestion(chIdx, sIdx)">
                                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false" width="16" height="16"><path d="M3 6h18v2H3V6zm2 3h14l-1 11H6L5 9zm3-6h6l1 2H7l1-2z"/></svg>
                                        </cdx-button>
                                    </div>
                                </div>
                            </div>

                        </div>

                        <div class="row-controls">
							<div class="suggestion-add">
								<cdx-button size="small" @click.prevent="addSuggestion(chIdx)">
									<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false" width="16" height="16" style="margin-right:6px"><path d="M11 11V6h2v5h5v2h-5v5h-2v-5H6v-2z"/></svg>
									{{ $options.i18n.addSuggestion }}
								</cdx-button>
							</div>

							<div class="chapter-controls">
								<cdx-button v-if="chIdx === chapters.length - 1" size="small" @click.prevent="addChapter">
									<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false" width="16" height="16" style="margin-right:6px"><path d="M11 11V6h2v5h5v2h-5v5h-2v-5H6v-2z"/></svg>
									{{ $options.i18n.addChapter }}
								</cdx-button>
								<cdx-button size="small" :disabled="chapters.length <= 1" @click.prevent="removeChapter(chIdx)">
									<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false" width="16" height="16" style="margin-right:6px"><path d="M3 6h18v2H3V6zm2 3h14l-1 11H6L5 9zm3-6h6l1 2H7l1-2z"/></svg>
									{{ $options.i18n.removeChapter }}
								</cdx-button>
							</div>
                        </div>
                    </div>
                </div>

                <!-- Step 1: Edit Draft -->
                <div v-else-if="currentStep === 1" class="review-tool-edit-step">
                    <h3>{{ $options.i18n.editHeading }}</h3>
                    <p class="review-tool-edit-step__instruction">{{ $options.i18n.editInstruction }}</p>
                    <cdx-text-area
                        v-model="editedDraft"
                        :placeholder="$options.i18n.editPlaceholder"
                        rows="16"
                    ></cdx-text-area>
                </div>

                <!-- Step 2: Preview -->
                <div v-else-if="currentStep === 2" class="review-tool-preview">
                    <h3>{{ $options.i18n.previewHeading }}</h3>
                    <div
                        v-if="previewHtml"
                        class="review-tool-preview-pre review-tool-preview-pre--html"
                        ref="previewHtmlHost"
                        v-html="previewHtml"
                    ></div>
                    <pre class="review-tool-preview-pre" v-else>{{ previewWikitext }}</pre>
                </div>

                <!-- Step 3: Diff & Save -->
                <div v-else-if="currentStep === 3" class="review-tool-diff">
                    <h3>{{ $options.i18n.diffHeading }}</h3>
                    <div
                        v-if="diffHtml"
                        class="review-tool-diff-pre review-tool-diff-pre--html"
                        ref="diffHtmlHost"
                        v-html="diffHtml"
                    ></div>
                    <div v-else>
                        <p>{{ $options.i18n.diffLoading }}</p>
                        <pre class="review-tool-diff-pre">{{ diffLines.join('\\n') }}</pre>
                    </div>
                </div>

            <template #footer>
                 <div class="review-tool-multistep-dialog__footer-left">
                     <cdx-button
                         v-if="showAnnotationLoaderButton"
                         weight="quiet"
                         :disabled="isLoadingAnnotations"
                         @click.prevent="loadAnnotationsIntoForm"
                     >
                         {{ $options.i18n.loadAnnotations }}
                     </cdx-button>
                    <cdx-button
                        v-if="showAnnotationLoaderButton"
                        weight="quiet"
                        @click.prevent="handleImportClick"
                    >
                        {{ $options.i18n.importFromFile }}
                    </cdx-button>
                    <input ref="annotationImportInput" type="file" accept="application/json,.json" style="display:none" @change="onAnnotationFileSelected" />
                 </div>
                <div class="review-tool-multistep-dialog__actions">
                    <cdx-button
                        v-if="defaultAction"
                        action="normal"
                        :disabled="defaultAction.disabled"
                        @click.prevent="onDefaultAction"
                    >
                        {{ defaultAction.label }}
                    </cdx-button>
                    <cdx-button
                        v-if="primaryAction"
                        :action="primaryAction.actionType"
                        :disabled="primaryAction.disabled"
                        @click.prevent="onPrimaryAction"
                    >
                        {{ primaryAction.label }}
                    </cdx-button>
                </div>
            </template>

			</cdx-dialog>
			`
      });
      registerCodexComponents(app, Codex);
      mountApp(app);
    }).catch((error) => {
      console.error("[ReviewTool] 無法加載 Codex:", error);
      mw.notify(state_default.convByVar({ hant: "無法加載對話框組件。", hans: "无法加载对话框组件。" }), {
        type: "error",
        title: "[ReviewTool]"
      });
    });
  }
  function openCheckWritingDialog() {
    if (getMountedApp()) removeDialogMount();
    createCheckWritingDialog();
  }

  // src/dom/utils.ts
  function createMwEditSectionButton(label, title, onClick) {
    const button = document.createElement("a");
    button.href = "#";
    button.className = "review-tool-button";
    button.textContent = label;
    button.setAttribute("title", title);
    button.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      onClick(e);
    };
    const leftBracket = document.createElement("span");
    leftBracket.className = "mw-editsection-bracket";
    leftBracket.textContent = " [";
    const rightBracket = document.createElement("span");
    rightBracket.className = "mw-editsection-bracket";
    rightBracket.textContent = "]";
    const buttonGroup = document.createElement("span");
    buttonGroup.className = "review-tool-button-group";
    buttonGroup.appendChild(leftBracket);
    buttonGroup.appendChild(button);
    buttonGroup.appendChild(rightBracket);
    return buttonGroup;
  }
  function getHeadingTitle(heading) {
    if (!heading) return null;
    const htmlHeading = heading instanceof HTMLHeadingElement ? heading : heading.querySelector("h1, h2, h3, h4, h5, h6");
    if (!htmlHeading) return null;
    if (htmlHeading.id) return htmlHeading.id;
    const innerWithId = htmlHeading.querySelector("[id]");
    if (innerWithId && innerWithId.id) return innerWithId.id;
    const threadId = htmlHeading.getAttribute && htmlHeading.getAttribute("data-mw-thread-id");
    if (threadId) return threadId;
    const text = htmlHeading.textContent && htmlHeading.textContent.trim();
    return text || null;
  }
  function appendButtonToHeading(heading, button) {
    const mwEditSection = heading.querySelector(".mw-editsection");
    if (!mwEditSection) return;
    try {
      const stateModule = (init_state(), __toCommonJS(state_exports));
      const state2 = stateModule && stateModule.default ? stateModule.default : stateModule;
      const anchor = button.querySelector && button.querySelector("a") || null;
      if (anchor && typeof anchor.onclick === "function") {
        const orig = anchor.onclick;
        anchor.onclick = (e) => {
          try {
            state2.pendingReviewHeading = heading;
          } catch (err) {
            console.error("[ReviewTool][appendButtonToHeading] failed to set pendingReviewHeading", err);
            throw err;
          }
          try {
            orig.call(anchor, e);
          } catch (ex) {
            console.error("[ReviewTool][appendButtonToHeading] original click handler failed", ex);
            throw ex;
          }
        };
      } else if (anchor) {
        anchor.addEventListener("click", (e) => {
          try {
            state2.pendingReviewHeading = heading;
          } catch (err) {
            console.error("[ReviewTool][appendButtonToHeading] failed to set pendingReviewHeading", err);
            throw err;
          }
        });
      }
    } catch (e) {
      console.error("[ReviewTool][appendButtonToHeading] failed to append button or import state", e);
      throw e;
    }
    mwEditSection.append(button);
  }
  function addVectorMenuTab(id, label, title, onClick, options) {
    const menu = document.getElementById("p-views");
    if (!menu) {
      console.warn("[ReviewTool] Vector menu #p-views not found");
      return null;
    }
    const list = menu.querySelector(".vector-menu-content-list");
    if (!list) {
      console.warn("[ReviewTool] Vector menu content list not found");
      return null;
    }
    if (document.getElementById(id)) {
      return document.getElementById(id);
    }
    const li = document.createElement("li");
    li.id = id;
    li.className = "vector-tab-noicon mw-list-item";
    if (options == null ? void 0 : options.selected) {
      li.classList.add("selected");
    }
    const a = document.createElement("a");
    a.href = "#";
    a.title = title;
    a.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      onClick(e);
    };
    const span = document.createElement("span");
    span.textContent = label;
    a.appendChild(span);
    li.appendChild(a);
    const watchLi = list.querySelector("#ca-watch");
    if (watchLi) {
      list.insertBefore(li, watchLi);
    } else {
      list.appendChild(li);
    }
    return li;
  }

  // src/dom/talk_page.ts
  function deriveSubjectArticleTitle(pageName) {
    var _a, _b;
    if (!pageName) {
      return "";
    }
    if (typeof mw !== "undefined" && ((_a = mw == null ? void 0 : mw.Title) == null ? void 0 : _a.newFromText)) {
      try {
        const talkTitle = mw.Title.newFromText(pageName);
        const subject = (_b = talkTitle == null ? void 0 : talkTitle.getSubjectPage) == null ? void 0 : _b.call(talkTitle);
        if (subject == null ? void 0 : subject.getPrefixedText) {
          return subject.getPrefixedText();
        }
      } catch (err) {
        console.warn("[ReviewTool] deriveSubjectArticleTitle failed to parse Title", err);
      }
    }
    const replacements = [
      { regex: /^User_talk:/i, replacement: "User:" },
      { regex: /^Wikipedia_talk:/i, replacement: "Wikipedia:" },
      { regex: /^Project_talk:/i, replacement: "Project:" },
      { regex: /^Template_talk:/i, replacement: "Template:" },
      { regex: /^Help_talk:/i, replacement: "Help:" },
      { regex: /^Category_talk:/i, replacement: "Category:" },
      { regex: /^Portal_talk:/i, replacement: "Portal:" },
      { regex: /^Draft_talk:/i, replacement: "Draft:" },
      { regex: /^Module_talk:/i, replacement: "Module:" },
      { regex: /^Talk:/i, replacement: "" }
    ];
    for (const { regex, replacement } of replacements) {
      if (regex.test(pageName)) {
        return pageName.replace(regex, replacement);
      }
    }
    return pageName;
  }
  function decideAssessmentType(articleTitle, sectionTitle) {
    let assessmentType = null;
    if (state_default.inTalkPage) {
      const sectionRegexes = getSectionRegexes();
      for (const [key, regex] of Object.entries(sectionRegexes)) {
        if (regex.test(sectionTitle)) {
          assessmentType = key;
          break;
        }
      }
    } else if (articleTitle === "Wikipedia:Wikipedia:優良條目評選") {
      assessmentType = "good";
    } else if (articleTitle === "Wikipedia:Wikipedia:典范条目评选") {
      assessmentType = "featured";
    } else if (articleTitle === "Wikipedia:Wikipedia:特色列表評選") {
      assessmentType = "featured_list";
    }
    return assessmentType;
  }
  function createReviewManagementButton(articleTitle, sectionTitle) {
    const assessmentType = decideAssessmentType(articleTitle, sectionTitle);
    return createMwEditSectionButton(state_default.convByVar({
      hant: "管理評審",
      hans: "管理评审"
    }), state_default.convByVar({ hant: "使用 ReviewTool 小工具管理評審", hans: "使用 ReviewTool 小工具管理评审" }), (e) => {
      state_default.articleTitle = articleTitle;
      state_default.assessmentType = assessmentType;
      openReviewManagementDialog();
    });
  }
  function createCheckWritingButton(articleTitle, sectionTitle) {
    const assessmentType = decideAssessmentType(articleTitle, sectionTitle);
    return createMwEditSectionButton(state_default.convByVar({
      hant: "檢查文筆",
      hans: "检查文笔"
    }), state_default.convByVar({ hant: "使用 ReviewTool 小工具檢查文筆", hans: "使用 ReviewTool 小工具检查文笔" }), (e) => {
      state_default.articleTitle = articleTitle;
      state_default.assessmentType = assessmentType;
      openCheckWritingDialog();
    });
  }
  function addTalkPageReviewToolButtonsToDOM(namespace, pageName) {
    var _a, _b;
    if (document.querySelector("#review-tool-buttons-added")) return;
    const allSectionHeadings = document.querySelectorAll(".mw-heading.mw-heading2");
    const titleObj = typeof mw !== "undefined" && ((_a = mw == null ? void 0 : mw.Title) == null ? void 0 : _a.newFromText) ? mw.Title.newFromText(pageName) : null;
    const isTalkPage = titleObj && typeof titleObj.isTalkPage === "function" && titleObj.isTalkPage() || (typeof namespace === "number" ? namespace % 2 === 1 : false);
    if (isTalkPage) {
      state_default.inTalkPage = true;
      const articleTitle = deriveSubjectArticleTitle(pageName);
      state_default.articleTitle = articleTitle;
      const relevantHeadings = Array.from(allSectionHeadings).filter((heading) => {
        const sectionTitle = getHeadingTitle(heading);
        if (!sectionTitle) return false;
        return Object.values(getSectionRegexes()).some((regex) => regex.test(sectionTitle));
      });
      relevantHeadings.forEach((heading) => {
        const sectionTitle = getHeadingTitle(heading);
        if (!sectionTitle) return;
        appendButtonToHeading(heading, createReviewManagementButton(articleTitle, sectionTitle));
        findAndAppendCheckWritingButton(heading, articleTitle, sectionTitle);
      });
    } else {
      state_default.inTalkPage = false;
      allSectionHeadings.forEach((heading) => {
        const sectionTitle = getHeadingTitle(heading);
        if (!sectionTitle) return;
        appendButtonToHeading(heading, createReviewManagementButton(sectionTitle, sectionTitle));
        findAndAppendCheckWritingButton(heading, sectionTitle, sectionTitle);
      });
    }
    const marker = document.createElement("div");
    marker.id = "review-tool-buttons-added";
    marker.style.display = "none";
    (_b = document.querySelector("#mw-content-text .mw-parser-output")) == null ? void 0 : _b.appendChild(marker);
  }
  function findAndAppendCheckWritingButton(heading2, articleTitle, sectionTitle) {
    let sibling = heading2.nextElementSibling;
    while (sibling && !sibling.classList.contains("mw-heading2")) {
      if (sibling.classList.contains("mw-heading")) {
        const secTitle = getHeadingTitle(sibling);
        if (secTitle && /文[筆笔]/.test(secTitle)) {
          appendButtonToHeading(sibling, createCheckWritingButton(articleTitle, sectionTitle));
        }
      }
      sibling = sibling.nextElementSibling;
    }
  }

  // src/dom/article_page.ts
  init_state();

  // src/dialogs/annotation_editor.ts
  init_state();
  function openAnnotationEditorDialog(options) {
    var _a;
    const dialogOptions = {
      sectionPath: options.sectionPath,
      sentenceText: options.sentenceText,
      initialOpinion: options.initialOpinion || "",
      mode: options.mode || "create",
      allowDelete: (_a = options.allowDelete) != null ? _a : options.mode === "edit"
    };
    if (getMountedApp()) removeDialogMount();
    return loadCodexAndVue().then(({ Vue, Codex }) => {
      return new Promise((resolve) => {
        let resolved = false;
        const finalize = (result) => {
          if (resolved) return;
          resolved = true;
          resolve(result);
        };
        const app = Vue.createMwApp({
          i18n: {
            titleCreate: state_default.convByVar({ hant: "新增批註", hans: "新增批注" }),
            titleEdit: state_default.convByVar({ hant: "編輯批註", hans: "编辑批注" }),
            sectionLabel: state_default.convByVar({ hant: "章節:", hans: "章节:" }),
            sentenceLabel: state_default.convByVar({ hant: "句子:", hans: "句子:" }),
            opinionLabel: state_default.convByVar({ hant: "批註內容", hans: "批注内容" }),
            opinionPlaceholder: state_default.convByVar({ hant: "請輸入批註內容…", hans: "请输入批注内容…" }),
            opinionRequired: state_default.convByVar({ hant: "批註內容不能為空", hans: "批注内容不能为空" }),
            cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
            save: state_default.convByVar({ hant: "儲存", hans: "保存" }),
            create: state_default.convByVar({ hant: "新增", hans: "新增" }),
            delete: state_default.convByVar({ hant: "刪除", hans: "删除" }),
            deleteConfirm: state_default.convByVar({ hant: "確定要刪除這條批註?", hans: "确定要删除这条批注?" })
          },
          data() {
            return {
              open: true,
              mode: dialogOptions.mode,
              sectionPath: dialogOptions.sectionPath,
              sentenceText: dialogOptions.sentenceText,
              opinion: dialogOptions.initialOpinion,
              allowDelete: dialogOptions.allowDelete,
              showValidationError: false
            };
          },
          computed: {
            dialogTitle() {
              return this.mode === "edit" ? this.$options.i18n.titleEdit : this.$options.i18n.titleCreate;
            },
            primaryLabel() {
              return this.mode === "edit" ? this.$options.i18n.save : this.$options.i18n.create;
            },
            canSave() {
              return Boolean((this.opinion || "").trim());
            }
          },
          watch: {
            opinion() {
              if (this.showValidationError && this.canSave) {
                this.showValidationError = false;
              }
            }
          },
          methods: {
            onPrimaryAction() {
              if (!this.canSave) {
                this.showValidationError = true;
                return;
              }
              finalize({ action: "save", opinion: this.opinion.trim() });
              this.closeDialog();
            },
            onCancelAction() {
              finalize({ action: "cancel" });
              this.closeDialog();
            },
            onDeleteClick() {
              if (!this.allowDelete) return;
              const ok = window.confirm(this.$options.i18n.deleteConfirm);
              if (!ok) return;
              finalize({ action: "delete" });
              this.closeDialog();
            },
            onUpdateOpen(newValue) {
              if (!newValue) {
                this.onCancelAction();
              }
            },
            closeDialog() {
              this.open = false;
              setTimeout(() => removeDialogMount(), 200);
            }
          },
          template: `
                        <cdx-dialog
                            v-model:open="open"
                            :title="dialogTitle"
                            :use-close-button="true"
                            @update:open="onUpdateOpen"
                            class="review-tool-dialog review-tool-annotation-editor-dialog"
                        >
                            <div class="review-tool-form-section">
                                <div class="review-tool-annotation-editor__label">{{ $options.i18n.sectionLabel }}</div>
                                <div class="review-tool-annotation-editor__section">{{ sectionPath }}</div>
                            </div>

                            <div class="review-tool-form-section">
                                <div class="review-tool-annotation-editor__label">{{ $options.i18n.sentenceLabel }}</div>
                                <div class="review-tool-annotation-editor__quote">{{ sentenceText }}</div>
                            </div>

                            <div class="review-tool-form-section">
                                <label class="review-tool-annotation-editor__label" :for="'annotation-opinion-input'">
                                    {{ $options.i18n.opinionLabel }}
                                </label>
                                <cdx-text-area
                                    id="annotation-opinion-input"
                                    v-model="opinion"
                                    rows="5"
                                    :placeholder="$options.i18n.opinionPlaceholder"
                                ></cdx-text-area>
                                <div v-if="showValidationError" class="review-tool-annotation-editor__error">
                                    {{ $options.i18n.opinionRequired }}
                                </div>
                            </div>

                            <template #footer>
                                <div class="review-tool-annotation-editor__footer">
                                    <cdx-button
                                        v-if="allowDelete"
                                        weight="quiet"
                                        action="destructive"
                                        class="review-tool-annotation-editor__delete"
                                        @click.prevent="onDeleteClick"
                                    >
                                        {{ $options.i18n.delete }}
                                    </cdx-button>
                                    <div class="review-tool-annotation-editor__actions">
                                        <cdx-button weight="quiet" @click.prevent="onCancelAction">
                                            {{ $options.i18n.cancel }}
                                        </cdx-button>
                                        <cdx-button
                                            action="progressive"
                                            :disabled="!canSave"
                                            @click.prevent="onPrimaryAction"
                                        >
                                            {{ primaryLabel }}
                                        </cdx-button>
                                    </div>
                                </div>
                            </template>
                        </cdx-dialog>
                    `
        });
        registerCodexComponents(app, Codex);
        mountApp(app);
      });
    }).catch((error) => {
      console.error("[ReviewTool] Failed to open annotation editor dialog", error);
      mw && mw.notify && mw.notify(
        state_default.convByVar({ hant: "無法開啟批註對話框。", hans: "无法开启批注对话框。" }),
        { type: "error", title: "[ReviewTool]" }
      );
      throw error;
    });
  }

  // src/dialogs/annotation_viewer.ts
  init_state();
  var viewerAppInstance = null;
  var viewerDialogOptions = null;
  function isAnnotationViewerDialogOpen() {
    return Boolean(viewerAppInstance);
  }
  function closeAnnotationViewerDialog() {
    if (viewerAppInstance) {
      viewerAppInstance.open = false;
      setTimeout(() => removeDialogMount(), 200);
      viewerAppInstance = null;
      viewerDialogOptions = null;
    }
  }
  function updateAnnotationViewerDialogGroups(groups) {
    if (viewerAppInstance) {
      viewerAppInstance.groups = groups;
    }
  }
  function openAnnotationViewerDialog(options) {
    viewerDialogOptions = {
      groups: options.groups || [],
      onEditAnnotation: options.onEditAnnotation,
      onDeleteAnnotation: options.onDeleteAnnotation,
      onClearAllAnnotations: options.onClearAllAnnotations
    };
    if (getMountedApp()) removeDialogMount();
    loadCodexAndVue().then(({ Vue, Codex }) => {
      const app = Vue.createMwApp({
        i18n: {
          title: state_default.convByVar({ hant: "批註列表", hans: "批注列表" }),
          empty: state_default.convByVar({ hant: "尚無批註", hans: "尚无批注" }),
          edit: state_default.convByVar({ hant: "編輯", hans: "编辑" }),
          delete: state_default.convByVar({ hant: "刪除", hans: "删除" }),
          deleteConfirm: state_default.convByVar({ hant: "確定刪除?", hans: "确定删除?" }),
          clearAll: state_default.convByVar({ hant: "清除全部", hans: "清除全部" }),
          clearAllConfirm: state_default.convByVar({ hant: "確定刪除所有批註?", hans: "确定删除所有批注?" }),
          clearAllDone: state_default.convByVar({ hant: "已清除所有批註。", hans: "已清除所有批注。" }),
          clearAllNothing: state_default.convByVar({ hant: "沒有可清除的批註。", hans: "没有可清除的批注。" }),
          clearAllError: state_default.convByVar({ hant: "清除批註時發生錯誤。", hans: "清除批注时发生错误。" }),
          sectionFallback: state_default.convByVar({ hant: "(未指定章節)", hans: "(未指定章节)" }),
          close: state_default.convByVar({ hant: "關閉", hans: "关闭" }),
          // export-related strings
          export: state_default.convByVar({ hant: "匯出", hans: "导出" }),
          exportDone: state_default.convByVar({ hant: "已匯出批註。", hans: "已导出批注。" }),
          exportError: state_default.convByVar({ hant: "匯出批註時發生錯誤。", hans: "导出批注时发生错误。" }),
          sortLabel: state_default.convByVar({ hant: "排序方式", hans: "排序方式" }),
          sortCreatedAsc: state_default.convByVar({ hant: "最早時間優先", hans: "最早时间优先" }),
          sortCreatedDesc: state_default.convByVar({ hant: "最新時間優先", hans: "最新时间优先" }),
          sortPosition: state_default.convByVar({ hant: "頁面位置", hans: "页面位置" })
        },
        data() {
          return {
            open: true,
            groups: (viewerDialogOptions == null ? void 0 : viewerDialogOptions.groups) || [],
            deletingAnnotationId: null,
            clearingAll: false,
            canClearAll: Boolean(viewerDialogOptions == null ? void 0 : viewerDialogOptions.onClearAllAnnotations),
            sortMethod: "position"
          };
        },
        computed: {
          isEmpty() {
            if (!Array.isArray(this.groups) || !this.groups.length) return true;
            return this.groups.every((group) => !group.annotations || group.annotations.length === 0);
          },
          flattenedAnnotations() {
            if (!Array.isArray(this.groups)) return [];
            const list = [];
            this.groups.forEach((group) => {
              if (!Array.isArray(group.annotations)) return;
              group.annotations.forEach((anno) => list.push(anno));
            });
            return list;
          },
          sortingOptions() {
            var _a;
            const i18n = ((_a = this.$options) == null ? void 0 : _a.i18n) || {};
            return [
              { value: "position", label: i18n.sortPosition || "頁面位置" },
              { value: "created-desc", label: i18n.sortCreatedDesc || "最新時間優先" },
              { value: "created-asc", label: i18n.sortCreatedAsc || "最早時間優先" }
            ];
          },
          sortedGroups() {
            if (this.sortMethod === "created-desc") {
              return this.buildTimeSortedGroups("desc");
            }
            if (this.sortMethod === "created-asc") {
              return this.buildTimeSortedGroups("asc");
            }
            return this.buildPositionSortedGroups();
          }
        },
        methods: {
          quotePreview(text) {
            if (!text) return "";
            const trimmed = text.trim();
            if (trimmed.length <= 60) return trimmed;
            return `${trimmed.slice(0, 57)}…`;
          },
          formatTimestamp(ts) {
            if (!ts) return "";
            try {
              return new Date(ts).toLocaleString();
            } catch (e) {
              return "";
            }
          },
          handleEdit(annotationId, sectionPath) {
            var _a;
            (_a = viewerDialogOptions == null ? void 0 : viewerDialogOptions.onEditAnnotation) == null ? void 0 : _a.call(viewerDialogOptions, annotationId, sectionPath);
          },
          handleDelete(annotationId, sectionPath) {
            if (!(viewerDialogOptions == null ? void 0 : viewerDialogOptions.onDeleteAnnotation)) return;
            const ok = window.confirm(this.$options.i18n.deleteConfirm);
            if (!ok) return;
            this.deletingAnnotationId = annotationId;
            Promise.resolve(viewerDialogOptions.onDeleteAnnotation(annotationId, sectionPath)).catch((error) => {
              console.error("[ReviewTool] Failed to delete annotation", error);
              mw && mw.notify && mw.notify(
                state_default.convByVar({ hant: "刪除批註時發生錯誤。", hans: "删除批注时发生错误。" }),
                { type: "error", title: "[ReviewTool]" }
              );
            }).finally(() => {
              this.deletingAnnotationId = null;
            });
          },
          handleClearAll() {
            if (!(viewerDialogOptions == null ? void 0 : viewerDialogOptions.onClearAllAnnotations) || this.isEmpty) return;
            const ok = window.confirm(this.$options.i18n.clearAllConfirm);
            if (!ok) return;
            this.clearingAll = true;
            Promise.resolve(viewerDialogOptions.onClearAllAnnotations()).then((result) => {
              const cleared = Boolean(result);
              if (mw && mw.notify) {
                mw.notify(
                  cleared ? this.$options.i18n.clearAllDone : this.$options.i18n.clearAllNothing,
                  { tag: "review-tool" }
                );
              }
            }).catch((error) => {
              console.error("[ReviewTool] Failed to clear annotations", error);
              mw && mw.notify && mw.notify(
                this.$options.i18n.clearAllError,
                { type: "error", title: "[ReviewTool]" }
              );
            }).finally(() => {
              this.clearingAll = false;
            });
          },
          handleExport() {
            if (this.isEmpty) return;
            try {
              const payload = {
                exportedAt: Date.now(),
                groups: this.groups
              };
              const json = JSON.stringify(payload, null, 2);
              const blob = new Blob([json], { type: "application/json;charset=utf-8" });
              const filename = `review-tool-annotations-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "")}.json`;
              const url = URL.createObjectURL(blob);
              const a = document.createElement("a");
              a.href = url;
              a.download = filename;
              document.body.appendChild(a);
              a.click();
              a.remove();
              URL.revokeObjectURL(url);
              if (mw && mw.notify) {
                mw.notify(this.$options.i18n.exportDone, { tag: "review-tool" });
              }
            } catch (error) {
              console.error("[ReviewTool] Failed to export annotations", error);
              mw && mw.notify && mw.notify(
                this.$options.i18n.exportError,
                { type: "error", title: "[ReviewTool]" }
              );
            }
          },
          buildPositionSortedGroups() {
            const buckets = /* @__PURE__ */ new Map();
            this.flattenedAnnotations.forEach((anno) => {
              const key = (anno.sectionPath || "").trim();
              if (!buckets.has(key)) buckets.set(key, []);
              buckets.get(key).push(anno);
            });
            const groups = Array.from(buckets.entries()).map(([sectionPath, annotations]) => ({
              sectionPath,
              annotations: annotations.slice().sort((a, b) => {
                const cmp = compareOrderKeys(a.sentencePos, b.sentencePos);
                if (cmp !== 0) return cmp;
                return (a.createdAt || 0) - (b.createdAt || 0);
              })
            }));
            groups.sort((a, b) => {
              const firstA = a.annotations[0];
              const firstB = b.annotations[0];
              const cmp = compareOrderKeys(firstA == null ? void 0 : firstA.sentencePos, firstB == null ? void 0 : firstB.sentencePos);
              if (cmp !== 0) return cmp;
              return (a.sectionPath || "").localeCompare(b.sectionPath || "");
            });
            return groups;
          },
          buildTimeSortedGroups(order) {
            const sorted = this.flattenedAnnotations.slice().sort((a, b) => {
              const delta = (a.createdAt || 0) - (b.createdAt || 0);
              return order === "asc" ? delta : -delta;
            });
            const groups = [];
            sorted.forEach((anno) => {
              const sectionPath = (anno.sectionPath || "").trim();
              const lastGroup = groups[groups.length - 1];
              if (!lastGroup || lastGroup.sectionPath !== sectionPath) {
                groups.push({ sectionPath, annotations: [anno] });
              } else {
                lastGroup.annotations.push(anno);
              }
            });
            return groups;
          },
          onUpdateOpen(newValue) {
            if (!newValue) {
              this.closeDialog();
            }
          },
          closeDialog() {
            this.open = false;
            setTimeout(() => {
              removeDialogMount();
              viewerAppInstance = null;
              viewerDialogOptions = null;
            }, 200);
          }
        },
        template: `
                    <cdx-dialog
                        v-model:open="open"
                        :title="$options.i18n.title"
                        :use-close-button="true"
                        :default-action="{ label: $options.i18n.close }"
                        @default="closeDialog"
                        @update:open="onUpdateOpen"
                        class="review-tool-dialog review-tool-annotation-viewer-dialog"
                    >
                        <div v-if="isEmpty" class="review-tool-annotation-viewer__empty">
                            {{ $options.i18n.empty }}
                        </div>
                        <div v-else class="review-tool-annotation-viewer__list">
                            <div
                                v-for="group in sortedGroups"
                                :key="group.sectionPath || 'default'"
                                class="review-tool-annotation-viewer__section"
                            >
                                <h4 class="review-tool-annotation-viewer__section-title">
                                    {{ group.sectionPath || $options.i18n.sectionFallback }}
                                </h4>
                                <ul class="review-tool-annotation-viewer__items">
                                    <li
                                        v-for="anno in group.annotations"
                                        :key="anno.id"
                                        class="review-tool-annotation-viewer__item"
                                    >
                                        <div class="review-tool-annotation-viewer__quote">“{{ quotePreview(anno.sentenceText) }}”</div>
                                        <div class="review-tool-annotation-viewer__opinion">{{ anno.opinion }}</div>
                                        <div class="review-tool-annotation-viewer__meta">
                                            {{ anno.createdBy }} · {{ formatTimestamp(anno.createdAt) }}
                                        </div>
                                        <div class="review-tool-annotation-viewer__actions">
                                            <cdx-button
                                                size="small"
                                                weight="quiet"
                                                @click.prevent="handleEdit(anno.id, group.sectionPath)"
                                            >
                                                {{ $options.i18n.edit }}
                                            </cdx-button>
                                            <cdx-button
                                                size="small"
                                                weight="quiet"
                                                action="destructive"
                                                :disabled="deletingAnnotationId === anno.id"
                                                @click.prevent="handleDelete(anno.id, group.sectionPath)"
                                            >
                                                {{ $options.i18n.delete }}
                                            </cdx-button>
                                        </div>
                                    </li>
                                </ul>
                            </div>
                        </div>
                        <template #footer>
                            <div class="review-tool-annotation-viewer__footer">
                                <div class="review-tool-annotation-viewer__footer-left">
                                    <cdx-select
                                        v-model:selected="sortMethod"
                                        :menu-items="sortingOptions"
                                        :disabled="isEmpty"
                                        :aria-label="$options.i18n.sortLabel"
                                        class="review-tool-annotation-viewer__sort-select"
                                    />
                                </div>
                                <div class="review-tool-annotation-viewer__footer-actions">
                                    <cdx-button
                                        v-if="!isEmpty"
                                        weight="quiet"
                                        @click.prevent="handleExport"
                                    >
                                        {{ $options.i18n.export }}
                                    </cdx-button>

                                    <cdx-button
                                        v-if="canClearAll && !isEmpty"
                                        action="destructive"
                                        weight="quiet"
                                        :disabled="clearingAll"
                                        @click.prevent="handleClearAll"
                                    >
                                        {{ $options.i18n.clearAll }}
                                    </cdx-button>
                                </div>
                            </div>
                        </template>
                    </cdx-dialog>
                `
      });
      registerCodexComponents(app, Codex);
      viewerAppInstance = mountApp(app);
    }).catch((error) => {
      console.error("[ReviewTool] Failed to open annotation viewer dialog", error);
      mw && mw.notify && mw.notify(
        state_default.convByVar({ hant: "無法開啟批註列表。", hans: "无法开启批注列表。" }),
        { type: "error", title: "[ReviewTool]" }
      );
    });
  }

  // src/dom/article_page.ts
  var floatingButton = null;
  var ANNOTATION_CONTAINER_CLASS = "review-tool-annotation-ui";
  var SENTENCE_CLASS = "sentence";
  var FLOATING_BUTTON_CLASS = "floating-button";
  var activeSectionStart = null;
  var activeSectionEnd = null;
  var activeSectionPath = null;
  var activePageName = null;
  var restrictSelectionToDescendants = false;
  var isMouseDown = false;
  var mouseDownPos = null;
  var HIDE_DELAY_MS = 180;
  var SELECTION_SHOW_DELAY_MS = 120;
  var selectionShowTimer = null;
  var floatingHideTimer = null;
  var inlineAnnotationBubbles = /* @__PURE__ */ new Map();
  function getCleanTextFromElement(el) {
    if (!el) return "";
    const clone = el.cloneNode(true);
    try {
      clone.querySelectorAll("sup.reference, sup.mw-ref, .reference, .mw-ref, .citation, .ref, .reference-text, .qeec-ref-tag-copy-btn").forEach((n) => n.remove());
      clone.querySelectorAll("[data-reference], [data-ref], .reference-note, .qeec-ref-tag-copy-btn").forEach((n) => n.remove());
    } catch (e) {
      console.error("[ReviewTool][getCleanTextFromElement] failed to remove decoration nodes", e);
      throw e;
    }
    const txt = clone.textContent || "";
    return txt.replace(/Copy permalink/g, "").replace(/\s+/g, " ").trim();
  }
  function removeDecorationsFromContainer(container) {
    try {
      container.querySelectorAll("sup.reference, sup.mw-ref, .reference, .mw-ref, .citation, .ref, .reference-text, .qeec-ref-tag-copy-btn, style, ipe-quick-edit").forEach((n) => n.remove());
      container.querySelectorAll("[data-reference], [data-ref], .reference-note, .qeec-ref-tag-copy-btn").forEach((n) => n.remove());
    } catch (e) {
      console.error("[ReviewTool][removeDecorationsFromContainer] failed", e);
      throw e;
    }
  }
  function getCleanTextFromRange(range) {
    if (!range) return "";
    const frag = range.cloneContents();
    const wrapper = document.createElement("div");
    wrapper.appendChild(frag);
    removeDecorationsFromContainer(wrapper);
    let txt = wrapper.textContent || "";
    txt = txt.replace(/Copy permalink/g, "");
    return txt.replace(/\s+/g, " ").trim();
  }
  function sanitizePlainText(text) {
    if (!text) return "";
    let s = text.replace(/\[\s*\d+\s*\]/g, "");
    s = s.replace(/[\u00B9\u00B2\u00B3\u2070-\u2079]+/g, "");
    return s.replace(/\s+/g, " ").trim();
  }
  function installSelectionListenersForSection(pageName, sectionStart, sectionEnd, sectionPath, restrictToDescendants = false) {
    uninstallSelectionListeners();
    activeSectionStart = sectionStart;
    activeSectionEnd = sectionEnd;
    activeSectionPath = sectionPath;
    activePageName = pageName;
    restrictSelectionToDescendants = restrictToDescendants;
    document.addEventListener("selectionchange", onSelectionChange);
    document.addEventListener("mouseup", onMouseUp);
    document.addEventListener("mousedown", onMouseDown);
    document.addEventListener("touchstart", onTouchStart, { passive: true });
    document.addEventListener("touchend", onTouchEnd);
  }
  function uninstallSelectionListeners() {
    document.removeEventListener("selectionchange", onSelectionChange);
    document.removeEventListener("mouseup", onMouseUp);
    document.removeEventListener("mousedown", onMouseDown);
    document.removeEventListener("touchstart", onTouchStart);
    document.removeEventListener("touchend", onTouchEnd);
    if (selectionShowTimer) {
      clearTimeout(selectionShowTimer);
      selectionShowTimer = null;
    }
    if (floatingHideTimer) {
      clearTimeout(floatingHideTimer);
      floatingHideTimer = null;
    }
    hideFloatingButton();
    document.documentElement.classList.remove("rt-selecting");
    activeSectionStart = null;
    activeSectionEnd = null;
    activeSectionPath = null;
    activePageName = null;
    restrictSelectionToDescendants = false;
  }
  function isNodeWithinSection(node) {
    if (!node || !activeSectionStart) return false;
    let el = null;
    if (node.nodeType === Node.TEXT_NODE) el = node.parentElement;
    else if (node instanceof Element) el = node;
    if (!el) return false;
    if (activeSectionStart === el || activeSectionStart.contains(el)) return true;
    if (!activeSectionEnd) {
      if (restrictSelectionToDescendants) {
        return false;
      }
      return (activeSectionStart.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
    }
    const startBeforeEl = (activeSectionStart.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
    const elBeforeEnd = (el.compareDocumentPosition(activeSectionEnd) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
    return startBeforeEl && elBeforeEnd;
  }
  function selectionInsideActiveSection() {
    const sel = document.getSelection();
    if (!sel || sel.isCollapsed) return null;
    const range = sel.getRangeAt(0);
    if (!activeSectionStart) return null;
    const startIn = isNodeWithinSection(range.startContainer);
    const endIn = isNodeWithinSection(range.endContainer);
    if (!startIn || !endIn) return null;
    return range;
  }
  function onMouseDown(e) {
    const target = e == null ? void 0 : e.target;
    if (target && floatingButton && (target === floatingButton || floatingButton.contains(target))) {
      return;
    }
    if (e) {
      mouseDownPos = { x: e.clientX, y: e.clientY };
    }
    isMouseDown = true;
    document.documentElement.classList.add("rt-selecting");
    hideFloatingButton();
  }
  function onMouseUp(e) {
    const target = e == null ? void 0 : e.target;
    if (target && floatingButton && (target === floatingButton || floatingButton.contains(target))) {
      isMouseDown = false;
      mouseDownPos = null;
      document.documentElement.classList.remove("rt-selecting");
      return;
    }
    isMouseDown = false;
    mouseDownPos = null;
    document.documentElement.classList.remove("rt-selecting");
    onSelectionChange();
  }
  function wasMouseDragged(e) {
    if (!mouseDownPos) return false;
    const dx = Math.abs(e.clientX - mouseDownPos.x);
    const dy = Math.abs(e.clientY - mouseDownPos.y);
    return dx > 3 || dy > 3;
  }
  function onTouchStart(e) {
    const target = e == null ? void 0 : e.target;
    if (target && floatingButton && (target === floatingButton || floatingButton.contains(target))) {
      return;
    }
    isMouseDown = true;
    document.documentElement.classList.add("rt-selecting");
    hideFloatingButton();
  }
  function onTouchEnd(e) {
    const target = e == null ? void 0 : e.target;
    if (target && floatingButton && (target === floatingButton || floatingButton.contains(target))) {
      isMouseDown = false;
      document.documentElement.classList.remove("rt-selecting");
      return;
    }
    isMouseDown = false;
    document.documentElement.classList.remove("rt-selecting");
    onSelectionChange();
  }
  function onSelectionChange() {
    if (isMouseDown) return;
    if (selectionShowTimer) {
      clearTimeout(selectionShowTimer);
      selectionShowTimer = null;
    }
    selectionShowTimer = window.setTimeout(() => {
      const selectionRange = selectionInsideActiveSection();
      if (!selectionRange) {
        hideFloatingButton();
        return;
      }
      const rect = selectionRange.getBoundingClientRect();
      const centerX = Math.max(40, Math.min(window.innerWidth - 40, rect.left + rect.width / 2));
      const topY = Math.max(8, rect.top + window.scrollY - 8);
      const rangeClone = selectionRange.cloneRange();
      showFloatingButton(centerX + window.scrollX, topY, () => {
        const selectedText = getCleanTextFromRange(selectionRange);
        if (!selectedText) {
          hideFloatingButton();
          return;
        }
        if (activePageName) {
          hideFloatingButton();
          const sel = document.getSelection();
          sel && sel.removeAllRanges();
          const computedSectionPath = computeSectionPathFromNode(selectionRange ? selectionRange.startContainer : null);
          const sentencePos = computeSentenceOrderKey(selectionRange ? selectionRange.startContainer : null);
          openAnnotationDialog(activePageName, null, computedSectionPath, {
            sentenceText: selectedText,
            selectionRange: rangeClone,
            sentencePos
          });
        }
      });
    }, SELECTION_SHOW_DELAY_MS);
  }
  function findAncestorSentence(node) {
    let cur = node;
    while (cur && cur !== document.body) {
      if (cur instanceof Element && cur.classList.contains(SENTENCE_CLASS)) return cur;
      cur = cur.parentNode;
    }
    return null;
  }
  function computeSentenceOrderKey(target) {
    const sentenceEl = (() => {
      if (!target) return null;
      if (target instanceof Element) {
        return target.classList.contains(SENTENCE_CLASS) ? target : findAncestorSentence(target);
      }
      return findAncestorSentence(target);
    })();
    if (!sentenceEl) return "";
    return getElementOrderKey(sentenceEl) || "";
  }
  function previousNode(node) {
    if (!node) return null;
    if (node.previousSibling) {
      let p = node.previousSibling;
      let pp = node.previousSibling;
      while (pp && pp.lastChild) pp = pp.lastChild;
      return pp;
    }
    return node.parentNode;
  }
  function findHeadingElementFromNode(node) {
    let cur = node;
    while (cur) {
      if (cur instanceof Element) {
        const el = cur;
        const tag = (el.tagName || "").toLowerCase();
        if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) return el;
        if (el.classList && el.classList.contains("mw-heading")) return el;
      }
      cur = cur.parentNode;
    }
    return null;
  }
  function getHeadingLevelAndTitle(el) {
    if (!el) return { level: null, title: null };
    const tag = (el.tagName || "").toLowerCase();
    if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
      const level = parseInt(tag.charAt(1), 10);
      const title = getHeadingTitle(el) || null;
      return { level, title };
    }
    const inner = el.querySelector("h1,h2,h3,h4,h5,h6");
    if (inner) {
      const lvl = parseInt((inner.tagName || "").charAt(1), 10);
      const title = getHeadingTitle(el) || getHeadingTitle(inner) || null;
      return { level: lvl, title };
    }
    const t = getHeadingTitle(el);
    return { level: null, title: t };
  }
  function computeSectionPathFromNode(startNode) {
    const pageFallback = state_default.articleTitle || state_default.convByVar({ hant: "導言", hans: "导言" });
    if (!startNode) return pageFallback;
    let anchor = startNode;
    if (anchor.nodeType === Node.TEXT_NODE) anchor = anchor.parentNode;
    if (!anchor) return pageFallback;
    const nearestByLevel = /* @__PURE__ */ new Map();
    let cur = anchor;
    while (cur) {
      cur = previousNode(cur);
      if (!cur) break;
      const hEl = findHeadingElementFromNode(cur);
      if (!hEl) continue;
      const info = getHeadingLevelAndTitle(hEl);
      if (!info.title || info.level === null) continue;
      if (info.level === 1) continue;
      if (nearestByLevel.has(info.level)) continue;
      nearestByLevel.set(info.level, info.title);
      if (info.level === 2) break;
    }
    if (nearestByLevel.size === 0) return pageFallback;
    const parts = [];
    for (let lvl = 2; lvl <= 6; lvl++) {
      if (nearestByLevel.has(lvl)) parts.push(nearestByLevel.get(lvl));
    }
    return parts.join("—");
  }
  function showFloatingButton(x, y, onClick) {
    if (!floatingButton) {
      floatingButton = document.createElement("button");
      floatingButton.className = `${ANNOTATION_CONTAINER_CLASS} ${FLOATING_BUTTON_CLASS}`;
      floatingButton.textContent = state_default.convByVar({ hant: "批註", hans: "批注" });
      floatingButton.onclick = (e) => {
        e.stopPropagation();
        e.preventDefault();
        onClick();
      };
      floatingButton.style.pointerEvents = "auto";
      document.body.appendChild(floatingButton);
    } else {
      floatingButton.onclick = (e) => {
        e.stopPropagation();
        e.preventDefault();
        onClick();
      };
    }
    if (floatingHideTimer) {
      clearTimeout(floatingHideTimer);
      floatingHideTimer = null;
    }
    floatingButton.style.position = "absolute";
    floatingButton.style.left = `${x}px`;
    floatingButton.style.top = `${y}px`;
    floatingButton.style.transform = "translate(-50%, -100%)";
    floatingButton.style.zIndex = "9999";
    floatingButton.style.display = "block";
    floatingButton.onmouseenter = () => {
      if (floatingHideTimer) {
        clearTimeout(floatingHideTimer);
        floatingHideTimer = null;
      }
    };
    floatingButton.onmouseleave = () => {
      if (floatingHideTimer) {
        clearTimeout(floatingHideTimer);
      }
      floatingHideTimer = window.setTimeout(() => hideFloatingButton(), HIDE_DELAY_MS);
    };
  }
  function hideFloatingButton() {
    if (floatingHideTimer) {
      clearTimeout(floatingHideTimer);
      floatingHideTimer = null;
    }
    if (floatingButton) {
      floatingButton.style.display = "none";
    }
  }
  function wrapSectionSentences(sectionStart, sectionEnd) {
    function shouldSkipElement(node) {
      if (node.nodeType !== Node.ELEMENT_NODE) return false;
      const el = node;
      if (el.classList.contains(ANNOTATION_CONTAINER_CLASS)) return true;
      if (el.hasAttribute("data-gadget") || el.hasAttribute("data-widget")) return true;
      const skipClasses = [
        "mw-editsection",
        "mw-indicator",
        "navbox",
        "infobox",
        "metadata",
        "noprint",
        "navigation",
        "catlinks",
        "printfooter",
        "mw-jump-link",
        "skin-",
        // prefix match for skin-specific elements
        "vector-",
        // prefix match for Vector skin elements
        "qeec-ref-tag-copy-btn",
        "ipe__in-article-link",
        "ipe-quick-edit",
        "ipe-quick-edit--create-only"
      ];
      for (const cls of skipClasses) {
        if (el.className && (el.classList.contains(cls) || typeof el.className === "string" && el.className.includes(cls))) {
          return true;
        }
      }
      if (el.id) {
        if (el.id.startsWith("mw-") || el.id.startsWith("footer-") || el.id.startsWith("p-") || el.id === "siteSub" || el.id === "contentSub") {
          return true;
        }
      }
      return false;
    }
    const sectionElements = [];
    if (sectionStart && sectionStart.childNodes && sectionStart.childNodes.length > 0 && !sectionEnd) {
      sectionStart.childNodes.forEach((n) => {
        if (!shouldSkipElement(n)) sectionElements.push(n);
        else console.log("[ReviewTool] Skipping element to preserve other scripts:", n);
      });
    } else {
      let cur = sectionStart.nextSibling;
      while (cur && cur !== sectionEnd) {
        if (!shouldSkipElement(cur)) {
          sectionElements.push(cur);
        } else {
          console.log("[ReviewTool] Skipping element to preserve other scripts:", cur);
        }
        cur = cur.nextSibling;
      }
    }
    let sentenceIndex = 0;
    console.log("[ReviewTool] wrapSectionSentences: processing", sectionElements.length, "child nodes");
    function splitTextIntoRanges(text) {
      const ranges = [];
      if (!text || !text.trim()) return ranges;
      const re = /[」』】〗〕\)\]\}\"'’”〉》]*[。!?\?\.\.\.\.\.\.\.!…]+[」』】〗〕\)\]\}\"'’”〉》]*/g;
      let lastIndex = 0;
      let m;
      while ((m = re.exec(text)) !== null) {
        const endPos = re.lastIndex;
        const part = text.slice(lastIndex, endPos);
        if (part.trim()) ranges.push({ start: lastIndex, end: endPos });
        lastIndex = endPos;
      }
      if (lastIndex < text.length) {
        const tail = text.slice(lastIndex);
        if (tail.trim()) ranges.push({ start: lastIndex, end: text.length });
      }
      if (ranges.length <= 1) {
        const alt = [];
        const altRe = /[」』】〗〕\)\]\}\"'’”〉》]*[。!?\?\.\.\.\.\.\.\.!…]+[」』】〗〕\)\]\}\"'’”〉》]*|(?:\r?\n)+|(?:\s{2,})/g;
        let last = 0;
        let mm;
        while ((mm = altRe.exec(text)) !== null) {
          const endPos = altRe.lastIndex;
          const part = text.slice(last, endPos);
          if (part.trim()) alt.push({ start: last, end: endPos });
          last = endPos;
        }
        if (last < text.length) {
          const tail2 = text.slice(last);
          if (tail2.trim()) alt.push({ start: last, end: text.length });
        }
        if (alt.length > 1) {
          return alt;
        }
      }
      return ranges;
    }
    function processElementRoot(root) {
      function isInlineElement(el) {
        if (!el || !el.tagName) return false;
        const t = el.tagName.toLowerCase();
        const inlineTags = /* @__PURE__ */ new Set([
          "a",
          "span",
          "em",
          "strong",
          "b",
          "i",
          "small",
          "sup",
          "sub",
          "code",
          "cite",
          "abbr",
          "time",
          "mark",
          "var",
          "img",
          "kbd"
        ]);
        return inlineTags.has(t);
      }
      const elementChildren = Array.from(root.childNodes).filter((n) => n.nodeType === Node.ELEMENT_NODE);
      const hasNonInlineElementChildren = elementChildren.some((el) => !isInlineElement(el));
      if (hasNonInlineElementChildren) {
        Array.from(root.childNodes).forEach((child) => {
          if (child.nodeType === Node.TEXT_NODE) {
            const textNode = child;
            const text = textNode.nodeValue || "";
            if (!text.trim()) return;
            const parts = splitTextIntoRanges(text).map((r) => text.slice(r.start, r.end)).filter((p) => p.trim());
            if (parts.length <= 1) {
              const span = document.createElement("span");
              span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
              span.setAttribute("data-sentence-index", String(sentenceIndex++));
              span.textContent = text;
              textNode.parentNode && textNode.parentNode.replaceChild(span, textNode);
            } else {
              const frag = document.createDocumentFragment();
              parts.forEach((part) => {
                const span = document.createElement("span");
                span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
                span.setAttribute("data-sentence-index", String(sentenceIndex++));
                span.textContent = part;
                frag.appendChild(span);
              });
              textNode.parentNode && textNode.parentNode.replaceChild(frag, textNode);
            }
          } else if (child.nodeType === Node.ELEMENT_NODE) {
            processElementRoot(child);
          }
        });
        return;
      }
      const filterNode = (node) => {
        if (node.nodeType !== Node.TEXT_NODE) return NodeFilter.FILTER_SKIP;
        let parent = node.parentElement;
        while (parent && parent !== root) {
          if (shouldSkipElement(parent)) {
            return NodeFilter.FILTER_REJECT;
          }
          parent = parent.parentElement;
        }
        return NodeFilter.FILTER_ACCEPT;
      };
      const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: filterNode });
      const segments = [];
      let acc = "";
      let tn = walker.nextNode();
      while (tn) {
        const t = tn.nodeValue || "";
        if (t) {
          segments.push({ node: tn, start: acc.length, end: acc.length + t.length });
          acc += t;
        }
        tn = walker.nextNode();
      }
      if (!segments.length) return;
      const ranges = splitTextIntoRanges(acc);
      const mapped = [];
      for (const r of ranges) {
        let startNode = null;
        let startOffset = 0;
        let endNode = null;
        let endOffset = 0;
        for (const seg of segments) {
          if (r.start >= seg.start && r.start <= seg.end) {
            startNode = seg.node;
            startOffset = r.start - seg.start;
          }
          if (r.end >= seg.start && r.end <= seg.end) {
            endNode = seg.node;
            endOffset = r.end - seg.start;
          }
          if (startNode && endNode) break;
        }
        if (startNode && endNode) {
          mapped.push({ startNode, startOffset, endNode, endOffset, absStart: r.start, absEnd: r.end });
        }
      }
      if (!mapped.length) return;
      mapped.sort((a, b) => b.absStart - a.absStart);
      let successCount = 0;
      for (const m of mapped) {
        if (m.absStart >= m.absEnd) continue;
        try {
          if (!m.startNode.isConnected || !m.endNode.isConnected) {
            console.warn("[ReviewTool] mapped nodes not connected, skipping", m);
            continue;
          }
          if (!root.contains(m.startNode) || !root.contains(m.endNode)) {
            console.warn("[ReviewTool] mapped nodes no longer in root, skipping", m);
            continue;
          }
          const range = document.createRange();
          range.setStart(m.startNode, m.startOffset);
          range.setEnd(m.endNode, m.endOffset);
          const frag = range.extractContents();
          const span = document.createElement("span");
          span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
          span.setAttribute("data-sentence-index", String(sentenceIndex++));
          span.appendChild(frag);
          range.insertNode(span);
          range.detach && range.detach();
          successCount++;
        } catch (e) {
          console.warn("[ReviewTool] range wrapping failed for one range, continuing", e, m);
        }
      }
      if (successCount === 0) {
        console.warn("[ReviewTool] no mapped ranges wrapped successfully, performing fallback wrapping for this root");
        const walker2 = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: filterNode });
        let tn2 = walker2.nextNode();
        while (tn2) {
          const text = tn2.nodeValue || "";
          if (!text.trim()) {
            tn2 = walker2.nextNode();
            continue;
          }
          const parts = text.split(new RegExp("(?<=[。!?!?;;】\\]}])\\s*", "g")).filter((p) => p.trim());
          if (parts.length <= 1) {
            const span = document.createElement("span");
            span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
            span.setAttribute("data-sentence-index", String(sentenceIndex++));
            span.textContent = text;
            tn2.parentNode && tn2.parentNode.replaceChild(span, tn2);
          } else {
            const frag = document.createDocumentFragment();
            parts.forEach((part) => {
              const span = document.createElement("span");
              span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
              span.setAttribute("data-sentence-index", String(sentenceIndex++));
              span.textContent = part;
              frag.appendChild(span);
            });
            tn2.parentNode && tn2.parentNode.replaceChild(frag, tn2);
          }
          tn2 = walker2.nextNode();
        }
      }
    }
    sectionElements.forEach((rootNode) => {
      if (rootNode.nodeType === Node.ELEMENT_NODE) {
        const el = rootNode;
        const tag = (el.tagName || "").toLowerCase();
        if (tag === "ul" || tag === "ol" || tag === "dl") {
          const items = Array.from(el.children).filter((c) => c.nodeType === Node.ELEMENT_NODE);
          items.forEach((item) => {
            processElementRoot(item);
          });
        } else {
          processElementRoot(el);
        }
      } else if (rootNode.nodeType === Node.TEXT_NODE) {
        const textNode = rootNode;
        const text = textNode.textContent || "";
        if (!text.trim()) return;
        const parts = text.split(new RegExp("(?<=[。!?!?;;」』】〗〕\\]]}}])\\s*", "g")).filter((p) => p.trim());
        if (parts.length <= 1) {
          const span = document.createElement("span");
          span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
          span.setAttribute("data-sentence-index", String(sentenceIndex++));
          span.textContent = text;
          textNode.parentNode && textNode.parentNode.replaceChild(span, textNode);
        } else {
          const frag = document.createDocumentFragment();
          parts.forEach((part) => {
            const span = document.createElement("span");
            span.className = `${ANNOTATION_CONTAINER_CLASS} ${SENTENCE_CLASS}`;
            span.setAttribute("data-sentence-index", String(sentenceIndex++));
            span.textContent = part;
            frag.appendChild(span);
          });
          textNode.parentNode && textNode.parentNode.replaceChild(frag, textNode);
        }
      }
    });
    try {
      attachSentenceClickHandlers(sectionStart, sectionEnd);
    } catch (e) {
      console.error("[ReviewTool] attachSentenceClickHandlers failed", e);
      throw e;
    }
    try {
      const selector = `.${ANNOTATION_CONTAINER_CLASS}.${SENTENCE_CLASS}`;
      const within = sectionStart.querySelectorAll ? sectionStart.querySelectorAll(selector) : document.querySelectorAll(selector);
      console.log("[ReviewTool] wrapSectionSentences: sentence spans found in section:", within.length);
    } catch (e) {
      console.error("[ReviewTool] counting sentence spans failed", e);
      throw e;
    }
  }
  function ensureWrappedSection(sectionStart, sectionEnd, attempts, delayMs) {
    if (!sectionStart) return;
    const sel = `.${ANNOTATION_CONTAINER_CLASS}.${SENTENCE_CLASS}`;
    function countSpans() {
      try {
        if (sectionEnd === null && sectionStart.querySelectorAll) {
          return sectionStart.querySelectorAll(sel).length;
        }
        const all = Array.from(document.querySelectorAll(sel));
        return all.filter((el) => sectionStart.contains(el)).length;
      } catch (e) {
        return 0;
      }
    }
    let schedule;
    if (Array.isArray(attempts)) schedule = attempts;
    else schedule = [0, 250, 750, 1500, 3e3, 5e3];
    if (!Array.isArray(attempts) && typeof attempts === "number") {
      schedule = schedule.slice(0, Math.max(1, attempts));
    }
    let idx = 0;
    function runOnce() {
      try {
        wrapSectionSentences(sectionStart, sectionEnd);
      } catch (e) {
        console.warn("[ReviewTool] ensureWrappedSection wrap failed", e);
      }
      const found = countSpans();
      console.log("[ReviewTool] ensureWrappedSection: attempt", idx + 1, "found", found);
      if (found > 0) return;
      idx++;
      if (idx < schedule.length) {
        setTimeout(runOnce, schedule[idx]);
      }
    }
    setTimeout(runOnce, schedule[0]);
  }
  function clearWrappedSentences() {
    document.querySelectorAll(`.${ANNOTATION_CONTAINER_CLASS}.${SENTENCE_CLASS}`).forEach((el) => {
      const parent = el.parentNode;
      if (!parent) return;
      const frag = document.createDocumentFragment();
      while (el.firstChild) {
        frag.appendChild(el.firstChild);
      }
      parent.replaceChild(frag, el);
    });
    document.querySelectorAll(".review-tool-annotation-badge").forEach((badge) => badge.remove());
    clearAllInlineAnnotationBubbles();
  }
  function clearAllInlineAnnotationBubbles() {
    inlineAnnotationBubbles.forEach((bubble) => {
      try {
        bubble.remove();
      } catch (e) {
        console.error("[ReviewTool] failed to remove inline annotation bubble", e, bubble);
      }
    });
    inlineAnnotationBubbles.clear();
    document.querySelectorAll(".review-tool-inline-annotation").forEach((bubble) => {
      bubble.remove();
    });
  }
  function createInlineAnnotationBubbleElement(pageName, sectionPath, annotationId, opinion) {
    const bubble = document.createElement("span");
    bubble.className = "review-tool-inline-annotation";
    bubble.dataset.annoId = annotationId;
    bubble.dataset.sectionPath = sectionPath;
    bubble.title = opinion;
    const icon = document.createElement("span");
    icon.className = "review-tool-inline-annotation__icon";
    icon.textContent = "💬";
    icon.title = opinion;
    icon.setAttribute("role", "button");
    icon.tabIndex = 0;
    icon.onclick = (event) => {
      event.preventDefault();
      event.stopPropagation();
      openAnnotationDialog(pageName, annotationId, sectionPath);
    };
    icon.onkeydown = (event) => {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        icon.click();
      }
    };
    bubble.appendChild(icon);
    return bubble;
  }
  function insertInlineAnnotationBubble(range, pageName, sectionPath, annotationId, opinion) {
    if (!range) {
      console.warn("[ReviewTool] Cannot insert inline annotation bubble without a selection range.");
      return;
    }
    removeInlineAnnotationBubble(annotationId);
    const bubble = createInlineAnnotationBubbleElement(pageName, sectionPath, annotationId, opinion);
    inlineAnnotationBubbles.set(annotationId, bubble);
    const insertionRange = range.cloneRange();
    insertionRange.collapse(false);
    insertionRange.insertNode(bubble);
  }
  function updateInlineAnnotationBubble(annotationId, opinion) {
    const bubble = inlineAnnotationBubbles.get(annotationId);
    if (!bubble) return;
    bubble.setAttribute("data-opinion", opinion);
    bubble.title = opinion;
    if (bubble.firstElementChild) {
      bubble.firstElementChild.title = opinion;
    }
  }
  function removeInlineAnnotationBubble(annotationId) {
    const bubble = inlineAnnotationBubbles.get(annotationId);
    if (!bubble) return;
    bubble.remove();
    inlineAnnotationBubbles.delete(annotationId);
  }
  async function openAnnotationDialog(pageName, annotationId, sectionPath, options = {}) {
    const selectionRange = options.selectionRange ? options.selectionRange.cloneRange() : null;
    const isEdit = annotationId !== null;
    const existingAnnotation = isEdit ? getAnnotation(pageName, annotationId) : null;
    const displaySentenceText = isEdit ? (existingAnnotation == null ? void 0 : existingAnnotation.sentenceText) || "" : sanitizePlainText(options.sentenceText || "");
    const initialOpinion = isEdit ? (existingAnnotation == null ? void 0 : existingAnnotation.opinion) || "" : "";
    const shouldReopenViewer = isAnnotationViewerDialogOpen();
    sectionPath = sectionPath === "目次" ? "序言" : sectionPath;
    try {
      if (shouldReopenViewer) {
        closeAnnotationViewerDialog();
      }
      const result = await openAnnotationEditorDialog({
        sectionPath,
        sentenceText: displaySentenceText,
        initialOpinion,
        mode: isEdit ? "edit" : "create",
        allowDelete: isEdit
      });
      if (!result || result.action === "cancel") {
        return;
      }
      if (result.action === "delete" && isEdit && annotationId) {
        const removed = deleteAnnotation(pageName, annotationId);
        if (removed) {
          removeInlineAnnotationBubble(annotationId);
        }
        return;
      }
      if (result.action === "save") {
        if (isEdit && annotationId) {
          const updated = updateAnnotation(pageName, annotationId, { opinion: result.opinion });
          if (updated) {
            updateInlineAnnotationBubble(annotationId, result.opinion);
          }
        } else {
          const sentencePosKey = options.sentencePos || computeSentenceOrderKey(selectionRange ? selectionRange.startContainer : null);
          const created = createAnnotation(pageName, sectionPath, displaySentenceText, result.opinion, sentencePosKey);
          insertInlineAnnotationBubble(selectionRange, pageName, sectionPath, created.id, result.opinion);
        }
      }
    } catch (error) {
      console.error("[ReviewTool] Failed to open annotation editor dialog", error);
    } finally {
      if (shouldReopenViewer) {
        showAnnotationViewer(pageName);
      }
    }
  }
  function attachSentenceClickHandlers(sectionStart, sectionEnd) {
    if (!sectionStart) return;
    if (!sectionEnd) {
      const spans = sectionStart.querySelectorAll(`.${ANNOTATION_CONTAINER_CLASS}.${SENTENCE_CLASS}`);
      spans.forEach((s) => attachHandlerToSpan(s));
      if (sectionStart.classList && sectionStart.classList.contains(ANNOTATION_CONTAINER_CLASS) && sectionStart.classList.contains(SENTENCE_CLASS)) {
        attachHandlerToSpan(sectionStart);
      }
      return;
    }
    let cur = sectionStart.nextSibling;
    while (cur && cur !== sectionEnd) {
      if (cur.nodeType === Node.ELEMENT_NODE) {
        const el = cur;
        el.querySelectorAll(`.${ANNOTATION_CONTAINER_CLASS}.${SENTENCE_CLASS}`).forEach((span) => {
          attachHandlerToSpan(span);
        });
        if (el.classList && el.classList.contains(ANNOTATION_CONTAINER_CLASS) && el.classList.contains(SENTENCE_CLASS)) {
          attachHandlerToSpan(el);
        }
      }
      cur = cur.nextSibling;
    }
    function attachHandlerToSpan(s) {
      if (s.dataset.clickAttached) return;
      s.dataset.clickAttached = "1";
      try {
        s.dataset._rtHandler = "1";
      } catch (e) {
      }
      if (attachHandlerToSpan._attachedCount === void 0) attachHandlerToSpan._attachedCount = 0;
      attachHandlerToSpan._attachedCount++;
      try {
        s.style.pointerEvents = "auto";
      } catch (e) {
        console.error("[ReviewTool] failed to set pointerEvents on sentence span", e, s);
        throw e;
      }
      try {
        s.style.cursor = "pointer";
      } catch (e) {
        console.error("[ReviewTool] failed to set cursor on sentence span", e, s);
        throw e;
      }
      const origBg = s.style.background;
      s.addEventListener("mouseenter", () => {
        try {
          s.style.background = "rgba(255,235,59,0.18)";
        } catch (e) {
          console.error("[ReviewTool] span mouseenter styling failed", e, s);
          throw e;
        }
      });
      s.addEventListener("mouseleave", () => {
        try {
          s.style.background = origBg || "";
        } catch (e) {
          console.error("[ReviewTool] span mouseleave styling failed", e, s);
          throw e;
        }
      });
      s.addEventListener("click", (e) => {
        if (wasMouseDragged(e)) {
          return;
        }
        const existingSelection = window.getSelection();
        if (existingSelection && !existingSelection.isCollapsed) {
          return;
        }
        e.stopPropagation();
        e.preventDefault();
        if (!activePageName || !activeSectionPath) return;
        const sentenceText = getCleanTextFromElement(s);
        const range = document.createRange();
        range.selectNodeContents(s);
        const rangeClone = range.cloneRange();
        const selection = window.getSelection();
        if (selection) {
          selection.removeAllRanges();
          selection.addRange(range);
        }
        const r = s.getBoundingClientRect();
        const centerX = Math.max(40, Math.min(window.innerWidth - 40, r.left + r.width / 2));
        const topY = Math.max(8, r.top + window.scrollY - 8);
        showFloatingButton(centerX + window.scrollX, topY, () => {
          if (activePageName) {
            hideFloatingButton();
            const sel = window.getSelection();
            sel && sel.removeAllRanges();
            const computedSectionPath = computeSectionPathFromNode(s);
            const sentencePos = computeSentenceOrderKey(s);
            openAnnotationDialog(activePageName, null, computedSectionPath, {
              sentenceText,
              selectionRange: rangeClone,
              sentencePos
            });
          }
        });
      });
    }
  }
  function showAnnotationViewer(pageName) {
    if (isAnnotationViewerDialogOpen()) {
      closeAnnotationViewerDialog();
      return;
    }
    const groups = buildAnnotationGroups(pageName);
    openAnnotationViewerDialog({
      groups,
      onEditAnnotation: (annotationId, sectionPath) => {
        openAnnotationDialog(pageName, annotationId, sectionPath);
      },
      onDeleteAnnotation: async (annotationId, sectionPath) => {
        const removed = deleteAnnotation(pageName, annotationId);
        if (removed) {
          removeInlineAnnotationBubble(annotationId);
          updateAnnotationViewerDialogGroups(buildAnnotationGroups(pageName));
        }
      },
      onClearAllAnnotations: async () => {
        const cleared = clearAnnotations(pageName);
        clearAllInlineAnnotationBubbles();
        updateAnnotationViewerDialogGroups(buildAnnotationGroups(pageName));
        return cleared;
      }
    });
  }
  function addMainPageReviewToolButtonsToDOM(pageName) {
    if (document.querySelector("#ca-annotate")) return;
    const tab = addVectorMenuTab("ca-annotate", state_default.convByVar({
      hant: "批註模式",
      hans: "批注模式"
    }), state_default.convByVar({
      hant: "切換批註模式",
      hans: "切换批注模式"
    }), (e) => toggleArticleAnnotationMode(pageName));
    addGlobalAnnotationViewerButton(pageName);
  }
  function addGlobalAnnotationViewerButton(pageName) {
    if (document.querySelector(".review-tool-global-button")) return;
    const btn = document.createElement("button");
    btn.className = "review-tool-global-button";
    btn.textContent = state_default.convByVar({ hant: "查看批註", hans: "查看批注" });
    btn.title = state_default.convByVar({ hant: "查看本頁所有批註", hans: "查看本页所有批注" });
    btn.style.position = "fixed";
    btn.style.bottom = "20px";
    btn.style.right = "20px";
    btn.style.zIndex = "10100";
    btn.style.padding = "10px 16px";
    btn.style.backgroundColor = "#36c";
    btn.style.color = "#fff";
    btn.style.border = "none";
    btn.style.borderRadius = "4px";
    btn.style.cursor = "pointer";
    btn.style.fontSize = "14px";
    btn.style.fontWeight = "bold";
    btn.style.boxShadow = "0 2px 8px rgba(0,0,0,0.2)";
    btn.style.display = "none";
    btn.onclick = () => {
      try {
        const fn = showAnnotationViewer;
        if (typeof fn === "function") {
          fn(state_default.articleTitle || pageName);
        } else {
          console.warn("[ReviewTool] showAnnotationViewer function not available");
        }
      } catch (e) {
        console.error("[ReviewTool] failed to open viewer", e);
        throw e;
      }
    };
    btn.onmouseenter = () => {
      btn.style.backgroundColor = "#447ff5";
    };
    btn.onmouseleave = () => {
      btn.style.backgroundColor = "#36c";
    };
    document.body.appendChild(btn);
  }
  function toggleArticleAnnotationMode(pageName) {
    const key = "__article__";
    state_default.toggleAnnotationModeState(key);
    if (state_default.isAnnotationModeActive(key)) {
      document.documentElement.classList.add("review-tool-annotation-mode");
      const tab = document.getElementById("ca-annotate");
      if (tab) {
        const span = tab.querySelector("a > span");
        if (span && span instanceof HTMLElement) span.style.fontWeight = "bold";
        tab.classList.add("selected");
      }
      mw && mw.notify && mw.notify(state_default.convByVar({
        hant: "批註模式已啟用。",
        hans: "批注模式已启用。"
      }), { tag: "review-tool" });
      console.log(`[ReviewTool] 條目「${state_default.articleTitle}」批註模式已啟用。`);
      const selectors = [
        "#mw-content-text .mw-parser-output",
        "#mw-content-text",
        ".mw-parser-output",
        "#content",
        "#bodyContent"
      ];
      let container = null;
      for (const s of selectors) {
        const el = document.querySelector(s);
        if (el) {
          container = el;
          break;
        }
      }
      if (container) {
        console.log("[ReviewTool] chosen content container:", container.tagName, container.id || "(no id)", container.className || "(no class)");
      } else {
        console.warn("[ReviewTool] could not find a content container with selectors", selectors);
      }
      if (!container) {
        console.warn("[ReviewTool] 未找到主要內容容器,無法啟用批註模式。");
        return;
      }
      const sectionPath = state_default.articleTitle || pageName;
      installSelectionListenersForSection(state_default.articleTitle || pageName, container, null, sectionPath, true);
      const tryCount = ensureWrappedSection ? ensureWrappedSection : wrapSectionSentences;
      tryCount(container, null, 4, 220);
      const gv = document.querySelector(".review-tool-global-button");
      if (gv) gv.style.display = "block";
    } else {
      console.log(`[ReviewTool] 條目「${state_default.articleTitle}」批註模式已停用。`);
      uninstallSelectionListeners();
      clearWrappedSentences();
      try {
        const tab = document.getElementById("ca-annotate");
        if (tab) {
          const span = tab.querySelector("a > span");
          if (span && span instanceof HTMLElement) span.style.fontWeight = "normal";
          tab.classList.remove("selected");
        }
      } catch (e) {
        console.error("[ReviewTool] failed to restore tab appearance", e);
        throw e;
      }
      try {
        document.documentElement.classList.remove("review-tool-annotation-mode");
      } catch (e) {
        console.error("[ReviewTool] failed to remove annotation mode class", e);
        throw e;
      }
      try {
        mw && mw.notify && mw.notify(state_default.convByVar({
          hant: "批註模式已停用。",
          hans: "批注模式已停用。"
        }), { tag: "review-tool" });
      } catch (e) {
        console.error("[ReviewTool] mw.notify failed", e);
        throw e;
      }
      try {
        const gv = document.querySelector(".review-tool-global-button");
        if (gv) gv.style.display = "none";
      } catch (e) {
        console.error("[ReviewTool] failed to hide global viewer button", e);
        throw e;
      }
    }
  }

  // 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>`;
      document.head.appendChild(div.firstChild);
    }
  }
  function init() {
    if (typeof document !== "undefined") {
      injectStyles(styles_default);
    }
    const namespace = mw.config.get("wgNamespaceNumber");
    const pageName = mw.config.get("wgPageName");
    const allowedNamespaces = [
      0,
      // 主
      1
      // 討論頁
    ];
    const allowedNamePrefixes = [
      "Wikipedia:同行评审",
      "Wikipedia:優良條目評選",
      "Wikipedia:典范条目评选",
      "Wikipedia:特色列表評選",
      "User:SuperGrey/gadgets/ReviewTool/TestPage",
      "User_talk:SuperGrey/gadgets/ReviewTool/TestPage"
    ];
    if (!allowedNamespaces.includes(namespace) && !allowedNamePrefixes.some((p) => pageName.startsWith(p))) {
      console.log("[ReviewTool] 不是目標頁面,小工具終止。");
      return;
    }
    state_default.initHanAssist().then(() => {
      if (namespace === 0 || pageName === "User:SuperGrey/gadgets/ReviewTool/TestPage") {
        state_default.articleTitle = pageName;
        mw.hook("wikipage.content").add(function() {
          addMainPageReviewToolButtonsToDOM(pageName);
        });
      } else {
        mw.hook("wikipage.content").add(function() {
          addTalkPageReviewToolButtonsToDOM(namespace, pageName);
        });
      }
    });
  }
  init();
})();
// </nowiki>