User:SuperGrey/gadgets/ReviewTool/main.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ 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>