跳转到内容

User:SuperGrey/gadgets/ACGATool/main.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/ACGATool]]

var ACGATool = {
    /**
     * 維基ACG專題獎提名規則。
     * @returns {object} 包含規則組的對象。
     */
    NominationRules: function () {
        var rules = [
            {
                group: ACGATool.convByVar({ hant: '(1) 內容擴充', hans: '(1) 内容扩充' }),
                rules: [
                    {
                        rule: '1a',
                        desc: ACGATool.convByVar({ hant: '短新增', hans: '短新增' }),
                        score: 1
                    },
                    {
                        rule: '1b',
                        desc: ACGATool.convByVar({ hant: '中新增', hans: '中新增' }),
                        score: 2
                    },
                    {
                        rule: '1c',
                        desc: ACGATool.convByVar({ hant: '長新增', hans: '长新增' }),
                        score: 3
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵編者新增或擴充條目內容。新內容達 2 kB 為「短新增」,達 3 kB 為「中新增」,達 5 kB 為「長新增」。', hans: '新增或扩充条目内容。新内容达 2 kB 为「短新增」,达 3 kB 为「中新增」,达 5 kB 为「长新增」。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(2) 品質提升', hans: '(2) 品质提升' }),
                rules: [
                    {
                        rule: '2-c',
                        desc: ACGATool.convByVar({ hant: '丙級', hans: '丙级' }),
                        score: 1
                    },
                    {
                        rule: '2-b',
                        desc: ACGATool.convByVar({ hant: '乙級', hans: '乙级' }),
                        score: 3
                    },
                    {
                        rule: '2-ga',
                        desc: ACGATool.convByVar({ hant: '優良', hans: '优良' }),
                        score: 5
                    },
                    {
                        rule: '2-fa',
                        desc: ACGATool.convByVar({ hant: '典特', hans: '典特' }),
                        score: 10
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵編者以條目品質評級為標準,提升條目品質。由缺失條目、小作品、初級提升至丙級為「丙級」,由丙級提升至乙級為「乙級」,由乙級提升至優良為「優良」,由優良提升至典範(特色)為「典特」。跨級品質提升,則多選所有符合項。', hans: '以条目品质评级为标准,提升条目品质。由缺失条目、小作品、初级提升至丙级为「丙级」,由丙级提升至乙级为「乙级」,由乙级提升至优良为「优良」,由优良提升至典范(特色)为「典特」。跨级品质提升,则多选所有符合项。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(3) 格式', hans: '(3) 格式' }),
                rules: [
                    {
                        rule: '3',
                        desc: ACGATool.convByVar({ hant: '格式', hans: '格式' }),
                        score: 1
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵編者創作整潔的條目。對於未滿 5 級創作獎的編者,條目應採用 ref 格式搭配 Cite 家族模板列出來源,引文沒有格式錯誤;對於已滿 5 級創作獎的有經驗編者,條目須行文通順、符合命名常規、遵循格式手冊。此項得分需依附於項目 1 或 2,不可單獨申請。', hans: '鼓励编辑者创作整洁的条目。对于未满 5 级创作奖的编辑者,条目应采用 ref 格式搭配 Cite 家族模板列出来源,引文没有格式错误;对于已满 5 级创作奖的有经验编辑者,条目须行文通顺、符合命名常规、遵循格式手册。此项得分需依附于项目 1 或 2,不可单独申请。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(4) 編輯活動', hans: '(4) 编辑活动' }),
                rules: [
                    {
                        rule: '4',
                        desc: ACGATool.convByVar({ hant: '活動', hans: '活动' }),
                        score: 1
                    },
                    {
                        rule: '4-req',
                        desc: ACGATool.convByVar({ hant: '請求', hans: '请求' }),
                        score: 1
                    },
                    {
                        rule: '4-dyk',
                        desc: ACGATool.convByVar({ hant: 'DYK', hans: 'DYK' }),
                        score: 1
                    },
                    {
                        rule: '4-req-game',
                        desc: ACGATool.convByVar({ hant: '請求·遊戲', hans: '请求·游戏' }),
                        score: 1
                    },
                    {
                        rule: '4-req-ac',
                        desc: ACGATool.convByVar({ hant: '請求·動漫', hans: '请求·动漫' }),
                        score: 1
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵編者參加社群或專題活動。可單獨申請的活動:新條目推薦、拯救典優條目。須依附於項目 1 或 2,不可單獨申請的活動:動漫及電子遊戲條目請求、基礎條目擴充挑戰、編輯松、動員令。提名時請手動編輯規則,註明所參與活動。', hans: '鼓励编辑者参加社群或专题活动。可单独申请的活动:新条目推荐、拯救典优条目。须依附于项目 1 或 2,不可单独申请的活动:动漫及电子游戏条目请求、基础条目扩充挑战、编辑松、动员令。提名时请手动编辑规则,注明所参与活动。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(5) 條目評審', hans: '(5) 条目评审' }),
                tabs: [
                    {
                        tab: ACGATool.convByVar({ hant: '泛用評審', hans: '泛用评审' }),
                        rules: [
                            {
                                rule: '5',
                                desc: ACGATool.convByVar({ hant: '評審', hans: '评审' }),
                                score: 1
                            },
                            {
                                rule: '5-half',
                                desc: ACGATool.convByVar({ hant: '快評', hans: '快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5a',
                                desc: ACGATool.convByVar({ hant: '文筆評審', hans: '文笔评审' }),
                                score: 1
                            },
                            {
                                rule: '5a-half',
                                desc: ACGATool.convByVar({ hant: '文筆快評', hans: '文笔快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5b',
                                desc: ACGATool.convByVar({ hant: '覆蓋面評審', hans: '覆盖面评审' }),
                                score: 1
                            },
                            {
                                rule: '5b-half',
                                desc: ACGATool.convByVar({ hant: '覆蓋面快評', hans: '覆盖面快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5c',
                                desc: ACGATool.convByVar({ hant: '來源格式評審', hans: '来源格式评审' }),
                                score: 1
                            },
                            {
                                rule: '5c-half',
                                desc: ACGATool.convByVar({ hant: '來源格式快評', hans: '来源格式快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5x',
                                desc: ACGATool.convByVar({ hant: '完整評審', hans: '完整评审' }),
                                score: 3
                            }
                        ]
                    },
                    {
                        tab: ACGATool.convByVar({ hant: '乙級(乙上級)內容評審', hans: '乙级(乙上级)内容评审' }),
                        rules: [
                            {
                                rule: '5-bcr',
                                desc: ACGATool.convByVar({ hant: '乙級評審', hans: '乙级评审' }),
                                score: 1
                            },
                            {
                                rule: '5-bcr-half',
                                desc: ACGATool.convByVar({ hant: '乙級快評', hans: '乙级快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5a-bcr',
                                desc: ACGATool.convByVar({ hant: '乙級文筆評審', hans: '乙级文笔评审' }),
                                score: 1
                            },
                            {
                                rule: '5a-bcr-half',
                                desc: ACGATool.convByVar({ hant: '乙級文筆快評', hans: '乙级文笔快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5b-bcr',
                                desc: ACGATool.convByVar({ hant: '乙級覆蓋面評審', hans: '乙级覆盖面评审' }),
                                score: 1
                            },
                            {
                                rule: '5b-bcr-half',
                                desc: ACGATool.convByVar({ hant: '乙級覆蓋面快評', hans: '乙级覆盖面快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5c-bcr',
                                desc: ACGATool.convByVar({ hant: '乙級來源格式評審', hans: '乙级来源格式评审' }),
                                score: 1
                            },
                            {
                                rule: '5c-bcr-half',
                                desc: ACGATool.convByVar({ hant: '乙級來源格式快評', hans: '乙级来源格式快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5x-bcr',
                                desc: ACGATool.convByVar({ hant: '完整乙級評審', hans: '完整乙级评审' }),
                                score: 3
                            }
                        ]
                    },
                    {
                        tab: ACGATool.convByVar({ hant: '優良內容評審', hans: '优良内容评审' }),
                        rules: [
                            {
                                rule: '5-gan',
                                desc: ACGATool.convByVar({ hant: '優良評審', hans: '优良评审' }),
                                score: 1
                            },
                            {
                                rule: '5-gan-half',
                                desc: ACGATool.convByVar({ hant: '優良快評', hans: '优良快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5a-gan',
                                desc: ACGATool.convByVar({ hant: '優良文筆評審', hans: '优良文笔评审' }),
                                score: 1
                            },
                            {
                                rule: '5a-gan-half',
                                desc: ACGATool.convByVar({ hant: '優良文筆快評', hans: '优良文笔快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5b-gan',
                                desc: ACGATool.convByVar({ hant: '優良覆蓋面評審', hans: '优良覆盖面评审' }),
                                score: 1
                            },
                            {
                                rule: '5b-gan-half',
                                desc: ACGATool.convByVar({ hant: '優良覆蓋面快評', hans: '优良覆盖面快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5c-gan',
                                desc: ACGATool.convByVar({ hant: '優良來源格式評審', hans: '优良来源格式评审' }),
                                score: 1
                            },
                            {
                                rule: '5c-gan-half',
                                desc: ACGATool.convByVar({ hant: '優良來源格式快評', hans: '优良来源格式快评' }),
                                score: 0.5
                            },
                            {
                                rule: '5x-gan',
                                desc: ACGATool.convByVar({ hant: '完整優良評審', hans: '完整优良评审' }),
                                score: 3
                            }
                        ]
                    },
                    {
                        tab: ACGATool.convByVar({ hant: '甲級內容評審', hans: '甲级内容评审' }),
                        rules: [
                            {
                                rule: '5-acr',
                                desc: ACGATool.convByVar({ hant: '甲級評審', hans: '甲级评审' }),
                                score: 2
                            },
                            {
                                rule: '5-acr-half',
                                desc: ACGATool.convByVar({ hant: '甲級快評', hans: '甲级快评' }),
                                score: 1
                            },
                            {
                                rule: '5a-acr',
                                desc: ACGATool.convByVar({ hant: '甲級文筆評審', hans: '甲级文笔评审' }),
                                score: 2
                            },
                            {
                                rule: '5a-acr-half',
                                desc: ACGATool.convByVar({ hant: '甲級文筆快評', hans: '甲级文笔快评' }),
                                score: 1
                            },
                            {
                                rule: '5b-acr',
                                desc: ACGATool.convByVar({ hant: '甲級覆蓋面評審', hans: '甲级覆盖面评审' }),
                                score: 2
                            },
                            {
                                rule: '5b-acr-half',
                                desc: ACGATool.convByVar({ hant: '甲級覆蓋面快評', hans: '甲级覆盖面快评' }),
                                score: 1
                            },
                            {
                                rule: '5c-acr',
                                desc: ACGATool.convByVar({ hant: '甲級來源格式評審', hans: '甲级来源格式评审' }),
                                score: 2
                            },
                            {
                                rule: '5c-acr-half',
                                desc: ACGATool.convByVar({ hant: '甲級來源格式快評', hans: '甲级来源格式快评' }),
                                score: 1
                            },
                            {
                                rule: '5x-acr',
                                desc: ACGATool.convByVar({ hant: '完整甲級評審', hans: '完整甲级评审' }),
                                score: 6
                            }
                        ]
                    },
                    {
                        tab: ACGATool.convByVar({ hant: '典範(特色)內容評審', hans: '典范(特色)内容评审' }),
                        rules: [
                            {
                                rule: '5-fac',
                                desc: ACGATool.convByVar({ hant: '典特評審', hans: '典特评审' }),
                                score: 2
                            },
                            {
                                rule: '5-fac-half',
                                desc: ACGATool.convByVar({ hant: '典特快評', hans: '典特快评' }),
                                score: 1
                            },
                            {
                                rule: '5a-fac',
                                desc: ACGATool.convByVar({ hant: '典特文筆評審', hans: '典特文笔评审' }),
                                score: 2
                            },
                            {
                                rule: '5a-fac-half',
                                desc: ACGATool.convByVar({ hant: '典特文筆快評', hans: '典特文笔快评' }),
                                score: 1
                            },
                            {
                                rule: '5b-fac',
                                desc: ACGATool.convByVar({ hant: '典特覆蓋面評審', hans: '典特覆盖面评审' }),
                                score: 2
                            },
                            {
                                rule: '5b-fac-half',
                                desc: ACGATool.convByVar({ hant: '典特覆蓋面快評', hans: '典特覆盖面快评' }),
                                score: 1
                            },
                            {
                                rule: '5c-fac',
                                desc: ACGATool.convByVar({ hant: '典特來源格式評審', hans: '典特来源格式评审' }),
                                score: 2
                            },
                            {
                                rule: '5c-fac-half',
                                desc: ACGATool.convByVar({ hant: '典特來源格式快評', hans: '典特来源格式快评' }),
                                score: 1
                            },
                            {
                                rule: '5x-fac',
                                desc: ACGATool.convByVar({ hant: '完整典特評審', hans: '完整典特评审' }),
                                score: 6
                            }
                        ]
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵維基人評審他人主編的條目。條目評審分為三大層面:文筆、覆蓋面、來源和格式。不完整評審(快評)折半得分。', hans: '鼓励维基人评审他人主编的条目。条目评审分为三大层面:文笔、覆盖面、来源和格式。不完整评审(快评)折半得分。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(6) 媒體', hans: '(6) 媒体' }),
                rules: [
                    {
                        rule: '6',
                        desc: ACGATool.convByVar({ hant: '媒體', hans: '媒体' }),
                        score: 3
                    },
                    {
                        rule: '6-fp',
                        desc: ACGATool.convByVar({ hant: '特色圖片', hans: '特色图片' }),
                        score: 5
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵編者上載與 ACG 專題相關的自由版權媒體檔案至 Wikimedia Commons。若檔案當選特色圖片,額外加 5 分。', hans: '鼓励编辑者上传与 ACG 专题相关的自由版权媒体文件至 Wikimedia Commons。若文件当选特色图片,额外加 5 分。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(7) 他薦', hans: '(7) 他荐' }),
                rules: [
                    {
                        rule: '7',
                        desc: ACGATool.convByVar({ hant: '他薦', hans: '他荐' }),
                        score: 0.5
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '鼓勵維基人提名他人。每有效提名他人 1 次得 0.5 分,每人每月最多得 5 分。若多次得分,請按照規則編輯自定義分數。', hans: '鼓励维基人提名他人。每有效提名他人 1 次得 0.5 分,每人每月最多得 5 分。若多次得分,请按照规则编辑自定义分数。' })
            },
            {
                group: ACGATool.convByVar({ hant: '(8) 其他', hans: '(8) 其他' }),
                rules: [
                    {
                        rule: '8',
                        desc: ACGATool.convByVar({ hant: '其他', hans: '其他' }),
                        score: 0
                    }
                ],
                explanation: ACGATool.convByVar({ hant: '獎勵維基人做出的其他難以量化的貢獻,如大量整理格式條目、上載作品封面、製作模板、制定寫作方案、維護資料庫等。請手動編輯規則名稱、自定義分數,並附上說明。最終具體得分將由與專題成員共同商討得出。', hans: '奖励维基人做出的其他难以量化的贡献,如大量整理格式条目、上传作品封面、制作模板、制定写作方案、维护数据库等。请手动编辑规则名称、自定义分数,并附上说明。最终具体得分将由与专题成员共同商讨得出。' })
            }
        ];
        return rules;
    },

    /**
     * 提名規則別名。
     * @returns {object} 包含別名的對象。
     */
    NominationRuleAliases: function () {
        return {
            '2a': '2-c',
            'c': '2-c',
            '2b': '2-b',
            'b': '2-b',
            '2c': '2-ga',
            'ga': '2-ga',
            '2d': '2-fa',
            'fa': '2-fa',
            '4a': '4-dyk',
            'dyk': '4-dyk',
            '4b': '4-req',
            '4b-game': '4-req-game',
            '4b-ac': '4-req-ac',
            'fp': '6-fp',
        };
    },

    /**
     * 提名規則集合。
     * @returns {object} 包含規則名稱和規則字典的對象。
     */
    NominationRuleSet: function () {
        var ruleNames = [];
        var ruleDict = {};
        for (var ruleGroup of ACGATool.NominationRules()) {
            if (ruleGroup.rules) {
                for (var ruleSet of ruleGroup.rules) {
                    ruleNames.push(ruleSet.rule);
                    ruleDict[ruleSet.rule] = ruleSet;
                }
            } else if (ruleGroup.tabs) {
                for (var tab of ruleGroup.tabs) {
                    for (var ruleSet of tab.rules) {
                        ruleNames.push(ruleSet.rule);
                        ruleDict[ruleSet.rule] = ruleSet;
                    }
                }
            }
        }
        return { ruleNames: ruleNames, ruleDict: ruleDict };
    },

    /**
     * 獲取完整的wikitext。
     * @returns {Promise<string>} 包含完整wikitext的Promise。
     */
    getFullText: async function () {
        var api = new mw.Api();
        var response = await api.get({
            action: 'query', titles: 'WikiProject:ACG/維基ACG專題獎/登記處', prop: 'revisions', rvslots: '*', rvprop: 'content', indexpageids: 1
        });
        var fulltext = response.query.pages[response.query.pageids[0]].revisions[0].slots.main['*'];
        return fulltext;
    },

    /**
     * Trims a token’s text and adjusts its absolute boundaries (skipping leading/trailing whitespace).
     * @param {object} token An object with properties { text, start, end }.
     * @returns {object} An object with properties { text, start, end } after trimming.
     */
    trimToken: function (token) {
        const leadingMatch = token.text.match(/^\s*/);
        const trailingMatch = token.text.match(/\s*$/);
        const leading = leadingMatch ? leadingMatch[0].length : 0;
        const trailing = trailingMatch ? trailingMatch[0].length : 0;
        return {
            text: token.text.trim(), start: token.start + leading, end: token.end - trailing
        };
    },

    /**
     * Splits the inner content of a template into tokens by detecting top-level "\n|" delimiters.
     * It scans the inner text (tracking nested braces) and splits only when it sees a newline followed by "|".
     * @param {string} innerContent The content between the outer "{{" and "}}".
     * @param {number} offset The absolute start position of innerContent in the wikitext.
     * @returns {Array} An array of tokens; each token is an object { text, start, end }.
     */
    splitParameters: function (innerContent, offset) {
        let tokens = [];
        let lastIndex = 0;
        let braceCount = 0;
        let i = 0;
        while (i < innerContent.length) {
            if (innerContent.substr(i, 2) === '{{') {
                braceCount++;
                i += 2;
                continue;
            }
            if (innerContent.substr(i, 2) === '}}') {
                braceCount = Math.max(braceCount - 1, 0);
                i += 2;
                continue;
            }
            // At top level, if we see a newline followed by a pipe, split here.
            if (braceCount === 0 && innerContent[i] === '\n' && innerContent[i + 1] === '|') {
                tokens.push({
                    text: innerContent.slice(lastIndex, i), start: offset + lastIndex, end: offset + i
                });
                i += 2; // skip the "\n|"
                lastIndex = i;
                continue;
            }
            i++;
        }
        tokens.push({
            text: innerContent.slice(lastIndex), start: offset + lastIndex, end: offset + innerContent.length
        });
        return tokens;
    },

    /**
     * Finds the matching closing braces "}}" for a template that starts at the given index.
     * @param {string} text The full wikitext.
     * @param {number} start The starting index where "{{" is found.
     * @returns {object} An object { endIndex } where endIndex is the index immediately after the closing "}}".
     */
    findTemplateEnd: function (text, start) {
        let braceCount = 0;
        let i = start;
        while (i < text.length) {
            if (text.substr(i, 2) === '{{') {
                braceCount++;
                i += 2;
                continue;
            }
            if (text.substr(i, 2) === '}}') {
                braceCount--;
                i += 2;
                if (braceCount === 0) break;
                continue;
            }
            i++;
        }
        return { endIndex: i };
    },

    /**
     * Parses a template starting at the given index in the wikitext.
     * For regular {{ACG提名}} templates, parameters are parsed as key=value pairs (with special handling for nested extras).
     * For simpler {{ACG提名2}} templates, parameters are parsed as key=value pairs. The keys are expected to have a trailing number (e.g. 條目名稱1, 用戶名稱1, etc.); entries are grouped by that number.
     * @param {string} text The full wikitext.
     * @param {number} start The starting index of the template (expects "{{").
     * @returns {object} An object { template, endIndex }.
     */
    parseTemplate: function (text, start) {
        const templateStart = start;
        const { endIndex: templateEnd } = ACGATool.findTemplateEnd(text, start);
        // Extract inner content (between outer "{{" and "}}").
        const innerStart = start + 2;
        const innerEnd = templateEnd - 2;
        const innerContent = text.slice(innerStart, innerEnd);
        // Split the inner content into tokens using the top-level "\n|" delimiter.
        const tokens = ACGATool.splitParameters(innerContent, innerStart);
        // The first token is the template name.
        let nameToken = ACGATool.trimToken(tokens[0]);
        let templateObj = {
            name: nameToken.text, nameLocation: { start: nameToken.start, end: nameToken.end }, params: {}, location: { start: templateStart, end: templateEnd }
        };

        if (templateObj.name.startsWith("ACG提名2")) {
            // For ACG提名2, process tokens as key=value pairs.
            // Group parameters by their trailing number.
            let kvGroups = {};
            for (let j = 1; j < tokens.length; j++) {
                let token = tokens[j];
                let tokenTrim = ACGATool.trimToken(token);
                if (tokenTrim.text === "") continue;
                const eqIndex = tokenTrim.text.indexOf('=');
                if (eqIndex === -1) continue;
                let rawKey = tokenTrim.text.substring(0, eqIndex);
                let rawValue = tokenTrim.text.substring(eqIndex + 1);
                let keyText = rawKey.trim();
                let valueText = rawValue.trim();
                let keyLeading = rawKey.match(/^\s*/)[0].length;
                let keyLocation = {
                    start: tokenTrim.start + keyLeading, end: tokenTrim.start + keyLeading + keyText.length
                };
                let valueLeading = rawValue.match(/^\s*/)[0].length;
                let valueLocation = {
                    start: tokenTrim.start + eqIndex + 1 + valueLeading, end: tokenTrim.end
                };
                // Expect keys in the form: prefix + number, e.g. "條目名稱1", "用戶名稱1", etc.
                let m = keyText.match(/^(.+?)(\d+)$/);
                if (m) {
                    let prefix = m[1].trim(); // e.g. "條目名稱"
                    let num = parseInt(m[2], 10);
                    if (!kvGroups[num]) kvGroups[num] = {};
                    kvGroups[num][prefix] = {
                        value: valueText, keyLocation: keyLocation, valueLocation: valueLocation, fullLocation: { start: token.start, end: token.end }
                    };
                } else {
                    // If the key doesn't match the expected pattern, store it under group "0".
                    if (!kvGroups["0"]) kvGroups["0"] = {};
                    kvGroups["0"][keyText] = {
                        value: valueText, keyLocation: keyLocation, valueLocation: valueLocation, fullLocation: { start: token.start, end: token.end }
                    };
                }
            }
            let entries = [];
            let groupNums = Object.keys(kvGroups).filter(k => k !== "0").map(Number).sort((a, b) => a - b);
            for (let num of groupNums) {
                let group = kvGroups[num];
                let allTokens = Object.values(group);
                let startPos = Math.min(...allTokens.map(t => t.fullLocation.start));
                let endPos = Math.max(...allTokens.map(t => t.fullLocation.end));
                group.fullLocation = { start: startPos, end: endPos };
                entries.push(group);
            }
            templateObj.entries = entries;
        } else {
            // For regular ACG提名, process tokens as key=value pairs (or positional parameters).
            for (let j = 1; j < tokens.length; j++) {
                let token = tokens[j];
                let tokenTrim = ACGATool.trimToken(token);
                if (tokenTrim.text === "") continue; // skip empty tokens
                const eqIndex = tokenTrim.text.indexOf('=');
                if (eqIndex !== -1) {
                    // Split into key and value without including the "=" in the value.
                    let rawKey = tokenTrim.text.substring(0, eqIndex);
                    let rawValue = tokenTrim.text.substring(eqIndex + 1);
                    let keyText = rawKey.trim();
                    let valueText = rawValue.trim();
                    // Compute absolute positions for key and value.
                    let keyLeading = rawKey.match(/^\s*/)[0].length;
                    let keyLocation = {
                        start: tokenTrim.start + keyLeading, end: tokenTrim.start + keyLeading + keyText.length
                    };
                    let valueLeading = rawValue.match(/^\s*/)[0].length;
                    let valueLocation = {
                        start: tokenTrim.start + eqIndex + 1 + valueLeading, end: tokenTrim.end
                    };
                    templateObj.params[keyText] = {
                        value: valueText, keyLocation: keyLocation, valueLocation: valueLocation, fullLocation: { start: token.start, end: token.end }
                    };
                } else {
                    // Positional parameter.
                    templateObj.params[j] = {
                        value: tokenTrim.text, fullLocation: { start: token.start, end: token.end }
                    };
                }
            }
            // Special handling for the "額外提名" parameter.
            if (templateObj.params['額外提名']) {
                const extraParam = templateObj.params['額外提名'];
                extraParam.nestedTemplates = ACGATool.parseMultipleTemplates(text, extraParam.valueLocation.start, extraParam.valueLocation.end);
            }
        }
        return { template: templateObj, endIndex: templateEnd };
    },

    /**
     * Parses nested extra templates from the given region of text.
     * This function uses a regex to capture any occurrence of "{{ACG提名/extra" that appears at
     * the beginning of the region or is preceded by a newline.
     * @param {string} text The full wikitext.
     * @param {number} regionStart The start index of the region.
     * @param {number} regionEnd The end index of the region.
     * @returns {Array} An array of parsed extra template objects.
     */
    parseMultipleTemplates: function (text, regionStart, regionEnd) {
        const templates = [];
        const regionText = text.slice(regionStart, regionEnd);
        // Regex: match either start of string (^) or a newline, then capture "{{ACG提名/extra"
        const regex = /(^|\n)({{ACG提名\/extra)/g;
        let match;
        while ((match = regex.exec(regionText)) !== null) {
            // Calculate the actual absolute start position of the extra template.
            let extraStart = regionStart + match.index + match[1].length;
            const { template, endIndex } = ACGATool.parseTemplate(text, extraStart);
            templates.push(template);
            // Advance regex.lastIndex so that we do not match inside the parsed template.
            regex.lastIndex = endIndex - regionStart;
        }
        return templates;
    },

    /**
     * Returns an array of date sections from the full wikitext.
     * Each section is determined by h3 headings of the form "=== date ===".
     * @param {string} text The full wikitext.
     * @returns {Array} Array of sections: { date, start, end }.
     */
    getDateSections: function (text) {
        const regex = /^===\s*(.+?)\s*===/gm;
        let sections = [];
        let matches = [];
        let match;
        while ((match = regex.exec(text)) !== null) {
            matches.push({ date: match[1].trim(), start: match.index, end: regex.lastIndex });
        }
        for (let i = 0; i < matches.length; i++) {
            const sectionStart = matches[i].start;
            const sectionDate = matches[i].date;
            const sectionEnd = (i < matches.length - 1) ? matches[i + 1].start : text.length;
            sections.push({ date: sectionDate, start: sectionStart, end: sectionEnd });
        }
        return sections;
    },

    /**
     * In a given date section (from h3 heading to before the next h3), collects all entries.
     * Each entry is either a main template ({{ACG提名}}), a nested extra template, or an entry from {{ACG提名2}}.
     * @param {string} text The full wikitext.
     * @param {object} section A section object { date, start, end }.
     * @returns {Array} Array of entry objects: { template, start, end, type }.
     */
    collectEntriesInSection: function (text, section) {
        let entries = [];
        const sectionText = text.slice(section.start, section.end);
        // Regex: match either {{ACG提名2 or {{ACG提名 (but not /extra)
        const regex = /{{(?:ACG提名2|ACG提名(?!\/extra))/g;
        let match;
        while ((match = regex.exec(sectionText)) !== null) {
            let absolutePos = section.start + match.index;
            let { template, endIndex } = ACGATool.parseTemplate(text, absolutePos);
            if (template.name.startsWith("ACG提名2")) {
                // For ACG提名2, add each grouped entry.
                if (template.entries) {
                    for (let entry of template.entries) {
                        entries.push({
                            template: entry, // entry holds the grouped key-value parameters
                            start: entry.fullLocation.start, end: entry.fullLocation.end, type: 'acg2'
                        });
                    }
                }
            } else {
                // For regular ACG提名
                entries.push({
                    template: template, start: template.location.start, end: template.location.end, type: 'main'
                });
                if (template.params['額外提名'] && template.params['額外提名'].nestedTemplates) {
                    for (let nested of template.params['額外提名'].nestedTemplates) {
                        entries.push({
                            template: nested, start: nested.location.start, end: nested.location.end, type: 'extra'
                        });
                    }
                }
            }
            // Advance regex.lastIndex to avoid re-parsing inside this template.
            regex.lastIndex = endIndex - section.start;
        }
        // Sort entries in order of appearance.
        entries.sort((a, b) => a.start - b.start);
        return entries;
    },

    /**
     * Queries the full wikitext for an entry given a specific date (from the h3 heading)
     * and an entry index (1-based, counting all entries in order).
     * Returns the entry object including its location.
     * @param {string} text The full wikitext.
     * @param {string} date The date string (e.g. "2月3日").
     * @param {number} index The 1-based index of the entry under that date.
     * @returns {object|null} The entry object { template, start, end, type } or null if not found.
     */
    queryEntry: function (text, date, index) {
        const sections = ACGATool.getDateSections(text);
        const targetSection = sections.find(sec => sec.date === date);
        if (!targetSection) return null;
        const entries = ACGATool.collectEntriesInSection(text, targetSection);
        if (index < 1 || index > entries.length) return null;
        return entries[index - 1]; // 1-based index
    },

    /**
     * Given an entry (from queryEntry) and a set of changes (mapping parameter key to new value),
     * updates only those parameter values in the original wikitext by using the precise location data.
     * This function does not replace the entire entry substring—only the changed parameter values.
     *
     * For main or extra entries, changes should be provided as an object where keys are parameter names
     * (e.g. "條目名稱") and values are the new text.
     *
     * For ACG提名2 entries, use keys like "條目名稱", "用戶名稱", "提名理由", "核對用", etc.
     *
     * @param {string} original The full original wikitext.
     * @param {object} entry The entry object (from queryEntry).
     * @param {object} changes An object mapping parameter keys to new values.
     * @returns {string} The updated wikitext.
     */
    updateEntryParameters: function (original, entry, changes) {
        let mods = [];
        if (entry.type === 'main' || entry.type === 'extra') {
            let params = entry.template.params;
            for (let key in changes) {
                if (params[key] && params[key].valueLocation) {
                    mods.push({
                        start: params[key].valueLocation.start, end: params[key].valueLocation.end, replacement: changes[key]
                    });
                }
            }
        } else if (entry.type === 'acg2') {
            // For ACG提名2 grouped entry, keys are like "條目名稱1", "用戶名稱1", etc.
            for (let key in changes) {
                if (entry.template[key]) {
                    let token = entry.template[key];
                    // Use token.valueLocation for precise updating.
                    mods.push({
                        start: token.valueLocation.start, end: token.valueLocation.end, replacement: changes[key]
                    });
                }
            }
        }
        // Apply modifications in descending order of start position.
        mods.sort((a, b) => b.start - a.start);
        let updated = original;
        for (let mod of mods) {
            updated = updated.slice(0, mod.start) + mod.replacement + updated.slice(mod.end);
        }
        return updated;
    },

    /**
     * Removes comments from the given text.
     * @param text {string} The text to remove comments from.
     * @returns {string} The text without comments.
     */
    removeComments: function (text) {
        text = text.replace(/<!--.*?-->/gs, '');
        return text.trim();
    },

    /**
     * 根據用戶的提名理由,解析出規則狀態。
     * @param reason {string} 用戶的提名理由。
     * @returns {object} 規則狀態。
     */
    parseUserReason: function (reason) {
        reason = ACGATool.removeComments(reason);
        var ruleStatus = {};
        if (reason.startsWith('{{ACG提名2/request|ver=1|')) {
            reason = reason.slice('{{ACG提名2/request|ver=1|'.length, -2);
        }
        var reasonList = reason.split(/\s+/);

        var ruleSet = ACGATool.NominationRuleSet();
        var ruleNames = ruleSet.ruleNames;
        var ruleDict = ruleSet.ruleDict;
        var ruleAliases = ACGATool.NominationRuleAliases();
        for (var rule of reasonList) {
            if (rule.endsWith('?')) {
                // 最後一個字符是?,可直接去掉?
                rule = rule.slice(0, -1);
            }

            // 名稱和分數的自定義
            var ruleNameMod = '', ruleScoreMod = '';
            while (rule.endsWith(')') || rule.endsWith(']')) {
                if (rule.endsWith(')')) {
                    var lastLeftParen = rule.lastIndexOf('(');
                    if (lastLeftParen === -1) break;
                    ruleNameMod = rule.slice(lastLeftParen + 1, -1);
                    rule = rule.slice(0, lastLeftParen);
                }
                if (rule.endsWith(']')) {
                    var lastLeftBracket = rule.lastIndexOf('[');
                    if (lastLeftBracket === -1) break;
                    ruleScoreMod = rule.slice(lastLeftBracket + 1, -1);
                    rule = rule.slice(0, lastLeftBracket);
                }
            }
            if (rule === '') continue;
            if (ruleAliases[rule]) {  // 是否是別名
                rule = ruleAliases[rule];
            }
            if (ruleNames.includes(rule)) {  // 是否在提名規則中
                ruleStatus[rule] = { selected: true };
                if (ruleNameMod !== '') {
                    ruleStatus[rule].desc = ruleNameMod;
                }
                if (ruleScoreMod !== '') {
                    ruleStatus[rule].score = parseFloat(ruleScoreMod);
                    if (isNaN(ruleStatus[rule].score)) {
                        // 分數不是數字,報錯
                        // console.log('分數不是數字', ruleScoreMod);
                        return null;
                    }
                }
            } else {
                // 不在提名規則中,報錯
                // console.log('不在提名規則中', rule);
                return null;
            }
        }
        return ruleStatus;
    },

    /**
     * 將queried(查詢結果)轉換為nomData(提名數據)。
     * @param queried {object} 查詢結果。
     * @returns {object} 提名數據。
     */
    queried2NomData: function (queried) {
        var nomData = {};

        if (queried.type === 'main' || queried.type === 'extra') {
            var params = queried.template.params;
            nomData.pageName = params['條目名稱'].value;
            nomData.awarder = params['用戶名稱'].value;
            var reasonWikitext = params['提名理由'].value;
            nomData.ruleStatus = ACGATool.parseUserReason(reasonWikitext);
            if (nomData.ruleStatus == null) {
                return null;
            }
            return nomData;
        } else if (queried.type === 'acg2') {
            var params = queried.template;
            nomData.pageName = ACGATool.removeComments(params['條目名稱'].value);
            nomData.awarder = ACGATool.removeComments(params['用戶名稱'].value);
            var reasonWikitext = params['提名理由'].value;
            nomData.ruleStatus = ACGATool.parseUserReason(reasonWikitext);
            // console.log('queried2NomData', nomData);
            if (nomData.ruleStatus == null) {
                return null;
            }
            return nomData;
        } else {
            return null;
        }
    },

    /**
     * 點擊編輯按鈕時的事件處理。
     * @param date 日期(章節標題)
     * @param index 該章節下第X個提名
     */
    editNonimation: function (date, index) {
        // console.log('editNonimation', date, index);

        ACGATool.getFullText().then(function (fulltext) {
            ACGATool.queried = ACGATool.queryEntry(fulltext, date, index);
            // console.log(ACGATool.queried);

            var nomData = ACGATool.queried2NomData(ACGATool.queried);
            if (nomData == null) {
                mw.notify(
                    ACGATool.convByVar({ hant: '小工具無法讀取該提名,請手動編輯。', hans: '小工具无法读取该提名,请手动编辑。' }),
                    { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                );
            } else {
                ACGATool.showEditNominationDialog(nomData);
            }
        });
    },

    /**
     * 點擊核對按鈕時的事件處理。
     * @param date 日期(章節標題)
     * @param index 該章節下第X個提名
     */
    checkNonimation: function (date, index) {
        // console.log('checkNonimation', date, index);

        ACGATool.getFullText().then(function (fulltext) {
            ACGATool.queried = ACGATool.queryEntry(fulltext, date, index);
            // console.log(ACGATool.queried);

            var nomData = ACGATool.queried2NomData(ACGATool.queried);
            if (nomData == null) {
                mw.notify(
                    ACGATool.convByVar({ hant: '小工具無法讀取該提名,請手動編輯。', hans: '小工具无法读取该提名,请手动编辑。' }),
                    { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                );
            } else {
                ACGATool.showCheckNominationDialog(nomData);
            }
        });
    },

    /**
     * 點擊登記新提名按鈕時的事件處理。
     */
    newNomination: function () {
        // console.log('newNomination');
        ACGATool.showNewNominationDialog();
    },

    /**
     * 點擊歸檔按鈕時的事件處理。
     */
    archiveChapter: function (date) {
        // console.log('archiveChapter', date);

        OO.ui.confirm(ACGATool.convByVar({ hant: '確定要歸檔「', hans: '确定要归档「' }) + date + ACGATool.convByVar({ hant: '」章節嗎?', hans: '」章节吗?' })).done(function (confirmed) {
            if (confirmed) {
                // console.log('confirmed');

                ACGATool.getFullText().then(function (fulltext) {
                    var sections = ACGATool.getDateSections(fulltext);
                    var targetSection = sections.find(sec => sec.date === date);
                    if (!targetSection) {
                        mw.notify(
                            ACGATool.convByVar({ hant: '小工具無法讀取該章節,請手動歸檔。', hans: '小工具无法读取该章节,请手动归档。' }),
                            { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                        );
                        return;
                    }
                    var sectionText = fulltext.slice(targetSection.start, targetSection.end);
                    var fulltextWithoutSection = fulltext.slice(0, targetSection.start) + fulltext.slice(targetSection.end);

                    // 找到包含年份的UTC字串,例如 2025年2月13日 (四) 20:58 (UTC)
                    var utcRegex = /提名人:.+?(\d{4})年(\d{1,2})月(\d{1,2})日 \((.*?)\) (\d{1,2}:\d{2}) \(UTC\)/;
                    var utcMatch = sectionText.match(utcRegex);
                    if (!utcMatch) {
                        mw.notify(
                            ACGATool.convByVar({ hant: '小工具無法讀取該章節的UTC時間,請手動歸檔。', hans: '小工具无法读取该章节的UTC时间,请手动归档。' }),
                            { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                        );
                        return;
                    }
                    // 獲得 X年Y月
                    var yearMonth = utcMatch[1] + '年' + utcMatch[2] + '月';
                    var archiveTarget = 'WikiProject:ACG/維基ACG專題獎/存檔/' + yearMonth;
                    
                    mw.notify(
                        ACGATool.convByVar({ hant: '小工具正在歸檔中,請耐心等待。', hans: '小工具正在归档中,请耐心等待。' }),
                        { type: 'info', title: ACGATool.convByVar({ hant: '提示', hans: '提示' }), autoHide: false }
                    );

                    var api = new mw.Api();
                    // 先檢查新的存檔頁面是否存在
                    var response = api.get({
                        action: 'query', titles: archiveTarget, prop: 'revisions', rvslots: '*', rvprop: 'content', indexpageids: 1
                    }).done(function (data) {
                        if (data.query.pageids[0] && (data.query.pages[data.query.pageids[0]].missing !== undefined || data.query.pages[data.query.pageids[0]].revisions[0].slots.main['*'].trim() === '')) {
                            // 新的存檔頁面不存在或者是空的
                            // 將存檔頁頭加入 sectionText
                            sectionText = '{{Talk archive|WikiProject:ACG/維基ACG專題獎/登記處}}\n\n' + sectionText;
                        } else {
                            // 直接歸檔,補充空行
                            sectionText = '\n\n' + sectionText;
                        }
                        api.postWithToken('csrf', {
                            action: 'edit', title: archiveTarget, appendtext: sectionText,
                            summary: '[[User:SuperGrey/gadgets/ACGATool|' + ACGATool.convByVar({ hant: '歸檔', hans: '归档' })
                                + ']]「' + date + '」' + ACGATool.convByVar({ hant: '章節', hans: '章节' }),
                        }).done(function (data) {
                            api.postWithToken('csrf', {
                                action: 'edit', title: 'WikiProject:ACG/維基ACG專題獎/登記處', text: fulltextWithoutSection,
                                summary: '[[User:SuperGrey/gadgets/ACGATool|' + ACGATool.convByVar({ hant: '歸檔', hans: '归档' })
                                    + ']]「' + date + '」' + ACGATool.convByVar({ hant: '章節至', hans: '章节至' }) + '「[[' + archiveTarget + ']]」',
                            }).done(function (data) {
                                mw.notify(
                                    ACGATool.convByVar({ hant: '小工具已歸檔', hans: '小工具已归档' }) + '「' + date + '」' + ACGATool.convByVar({ hant: '章節至', hans: '章节至' }) + '「[[' + archiveTarget + ']]」',
                                    { type: 'success', title: ACGATool.convByVar({ hant: '成功', hans: '成功' }) }
                                );

                                // 刷新頁面
                                ACGATool.refreshPage();
                            }).fail(function (error) {
                                console.log(error);
                                mw.notify(
                                    ACGATool.convByVar({ hant: '小工具無法歸檔,請手動歸檔。', hans: '小工具无法归档,请手动归档。' }),
                                    { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                                );
                            });
                        }).fail(function (error) {
                            console.log(error);
                            mw.notify(
                                ACGATool.convByVar({ hant: '小工具無法歸檔,請手動歸檔。', hans: '小工具无法归档,请手动归档。' }),
                                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                            );
                        });
                    });
                });
            } else {
                // console.log('cancelled');
            }
        });
    },

    /**
     * 在頁面上添加編輯按鈕。
     */
    addEditButtonsToPage: function () {
        // 找到<span role"button">登記新提名</span>
        var newNominationButton = $('span[role="button"]').filter(function () {
            return $(this).text() === '登記新提名' || $(this).text() === '登记新提名';
        });
        if (newNominationButton.length > 0) {
            // 修改原本按鈕的文本為「手動登記新提名」
            newNominationButton.text(ACGATool.convByVar({ hant: '手動登記新提名', hans: '手动登记新提名' }));
            newNominationButton.removeClass('mw-ui-progressive');

            // 父節點的父節點是<span>,在後面加入編輯按鈕
            var newNominationButtonParent = newNominationButton.parent().parent();
            var editUIButton = $('<span>').addClass('mw-ui-button').addClass('mw-ui-progressive').attr('role', 'button').text(ACGATool.convByVar({ hant: '登記新提名', hans: '登记新提名' }));
            var editButton = $('<a>').attr('href', 'javascript:void(0)').append(editUIButton).click(ACGATool.newNomination);
            newNominationButtonParent.append(' ').append(editButton);
        }

        // 識別所有h3
        $('div.mw-heading3').each(function () {
            var h3div = $(this);
            var h3 = h3div.find('h3').first();
            var date = h3.text().trim();
            var index = 0;

            // console.log('h3', date);

            // 為h3div底下的span.mw-editsection添加歸檔按鈕
            var editsection = h3div.find('span.mw-editsection').first();
            var editsectionA = editsection.find('a').first();
            $('<a>').attr('href', 'javascript:void(0)').click(function () {
                ACGATool.archiveChapter(date);
            }).append(ACGATool.convByVar({ hant: '歸檔', hans: '归档' })).insertAfter(editsectionA);
            $('<span>&nbsp;|&nbsp;</span>').insertAfter(editsectionA);

            h3div.nextUntil('div.mw-heading3', 'table.acgnom-table').each(function () {
                // console.log('table', date, index);
                var table = $(this);
                var rows = table.find('tr').slice(1);  // 去掉表頭
                var title = "";
                rows.each(function () {
                    var row = $(this);
                    var th = row.find('th');
                    if (th.length != 0) {
                        // 提名行
                        var nomEntry = th.first();
                        var nomEntryA = nomEntry.find('a');
                        if (nomEntryA.length != 0) {
                            title = nomEntry.find('a').first().attr('title');
                        } else {
                            title = nomEntry.text().trim();
                        }
                        ++index;
                        // console.log('nom', date, index, title);

                        // 加入編輯按鈕
                        var editIcon = $('<img>').attr('src', 'https://upload.wikimedia.org/wikipedia/commons/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg').css({ 'width': '12px' });
                        const currentIndex = index;
                        var editButton = $('<a>').attr('href', 'javascript:void(0)').append(editIcon).click(function () {
                            ACGATool.editNonimation(date, currentIndex);
                        });
                        nomEntry.append(' ').append(editButton);
                    } else {
                        // 核對行
                        var td = row.find('td').first();
                        var mwNoTalk = td.find('.mw-notalk').first();

                        var checkIcon = $('<img>').attr('src', 'https://upload.wikimedia.org/wikipedia/commons/3/30/OOjs_UI_icon_highlight-progressive.svg').css({ 'width': '12px' });
                        const currentIndex = index;
                        var checkButton = $('<a>').attr('href', 'javascript:void(0)').append(checkIcon).click(function () {
                            ACGATool.checkNonimation(date, currentIndex);
                        });
                        mwNoTalk.append(' ').append(checkButton);
                    }
                });
            });
        });
    },

    /**
     * 動態寬度文本輸入框。
     * @param config
     * @constructor
     */
    DynamicWidthTextInputWidget: function (config) {
        ACGATool.DynamicWidthTextInputWidget.parent.call(this, config);

        // Create a hidden element for measuring text width.
        this.$measure = $('<span>').css({
            position: 'absolute', visibility: 'hidden', whiteSpace: 'pre', // It’s important to mirror the font styles of the input.
            fontSize: '14px', fontFamily: 'sans-serif'
        }).appendTo(document.body);

        // Bind the adjustWidth function to the 'input' event.
        this.$input.on('input', this.adjustWidth.bind(this));
    },

    /**
     * 初始化動態寬度文本輸入框。在腳本啟動時執行一次。
     */
    initDynamicWidthTextInputWidget: function () {
        OO.inheritClass(ACGATool.DynamicWidthTextInputWidget, OO.ui.TextInputWidget);

        // add stylesheet for the widget
        mw.util.addCSS('.DynamicWidthTextInputWidget { display: inline-block; vertical-align: baseline; width: auto; margin: 0; } .DynamicWidthTextInputWidget input { height: 20px !important; border: none !important; border-bottom: 2px solid #ccc !important; padding: 0 !important; text-align: center; } .DynamicWidthTextInputWidget input:focus { outline: none !important; box-shadow: none !important; border-bottom: 2px solid #36c !important; } .DynamicWidthTextInputWidget input:disabled { background-color: transparent !important; color: #101418 !important; -webkit-text-fill-color: #101418 !important; text-shadow: none !important; border-bottom: 2px solid #fff !important; }');

        ACGATool.DynamicWidthTextInputWidget.prototype.adjustWidth = function () {
            // Get the current value; use placeholder if empty.
            var text = this.getValue() || '';
            // Update the measurement element.
            this.$measure.text(text);
            var newWidth = this.$measure.width() + 5; // Add a bit of padding.
            // Apply the new width to the input element.
            this.$input.css('width', newWidth + 'px');
        };
    },

    /**
     * 規則選框,附帶規則名和分數輸入框。
     * @param config
     * @constructor
     */
    RuleCheckboxInputWidget: function (config) {
        ACGATool.RuleCheckboxInputWidget.parent.call(this, config);
        this.ruleset = config.ruleset;
        this.nomidx = config.nomidx;
        this.check = config.check || false;
        if (!ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule]) {
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule] = {
                selected: this.isSelected(), desc: this.ruleset.desc, ogDesc: this.ruleset.desc, score: this.ruleset.score, maxScore: this.ruleset.score
            }
        } else {
            this.setSelected(ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].selected);
            if (!ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].desc) {
                ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].desc = this.ruleset.desc;
            }
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].ogDesc = this.ruleset.desc;
            if (!ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].score) {
                ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].score = this.ruleset.score;
            }
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].maxScore = this.ruleset.score;
        }
        this.ruleInputWidget = new ACGATool.DynamicWidthTextInputWidget({
            value: ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].desc, disabled: this.check ? false : (!this.isSelected())
        });
        this.ruleInputWidget.$element.addClass('DynamicWidthTextInputWidget');
        this.ruleInputWidget.$element.css({
            'margin-left': '5px'
        });
        this.ruleInputWidget.adjustWidth();
        this.leftBracketLabelWidget = new OO.ui.LabelWidget({
            label: '('
        });
        this.leftBracketLabelWidget.$element.css({
            'vertical-align': 'baseline', 'border-bottom': '2px solid #fff'
        });
        this.scoreInputWidget = new ACGATool.DynamicWidthTextInputWidget({
            value: ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].score, disabled: this.check ? false : (!this.isSelected())
        });
        this.scoreInputWidget.$element.addClass('DynamicWidthTextInputWidget');
        this.scoreInputWidget.adjustWidth();
        this.rightBracketLabelWidget = new OO.ui.LabelWidget({
            label: '分)'
        });
        this.rightBracketLabelWidget.$element.css({
            'vertical-align': 'baseline', 'border-bottom': '2px solid #fff', 'margin-right': '10px'
        });

        this.$element.append(this.ruleInputWidget.$element);
        this.$element.append(this.leftBracketLabelWidget.$element);
        this.$element.append(this.scoreInputWidget.$element);
        this.$element.append(this.rightBracketLabelWidget.$element);
        this.on('change', this.handleCheckboxChange.bind(this));
        this.ruleInputWidget.on('change', this.handleRuleInputChange.bind(this));
        this.scoreInputWidget.on('change', this.handleScoreInputChange.bind(this));
    },

    /**
     * 初始化規則選框。在腳本啟動時執行一次。
     */
    initRuleCheckboxInputWidget: function () {
        OO.inheritClass(ACGATool.RuleCheckboxInputWidget, OO.ui.CheckboxInputWidget);

        ACGATool.RuleCheckboxInputWidget.prototype.handleCheckboxChange = function (isChecked) {
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].selected = isChecked;
            var disableCheck = this.check ? false : (!isChecked);
            this.ruleInputWidget.setDisabled(disableCheck);
            this.scoreInputWidget.setDisabled(disableCheck);
            if (disableCheck) {
                // 取消選取時,重設規則名和分數
                this.ruleInputWidget.setValue(this.ruleset.desc);
                this.ruleInputWidget.adjustWidth();
                this.scoreInputWidget.setValue(this.ruleset.score);
                this.scoreInputWidget.adjustWidth();
            }
        }
        ACGATool.RuleCheckboxInputWidget.prototype.handleRuleInputChange = function (newValue) {
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].desc = newValue;
            // console.log(ACGATool.nominations[this.nomidx].ruleStatus);
        }
        ACGATool.RuleCheckboxInputWidget.prototype.handleScoreInputChange = function (newValue) {
            ACGATool.nominations[this.nomidx].ruleStatus[this.ruleset.rule].score = parseFloat(newValue);
            // console.log(ACGATool.nominations[this.nomidx].ruleStatus);
        }
    },

    /**
     * 生成提名表單。
     * @param {object} nomData 提名資料(非必須)。無資料時為新提名。
     * @returns {OO.ui.FieldsetLayout} 提名表單。
     */
    generateNominationFieldset: function (nomData) {
        var awarder, pageName, ruleStatus;
        if (nomData) {
            awarder = nomData.awarder;
            pageName = nomData.pageName;
            ruleStatus = nomData.ruleStatus;
        } else {
            awarder = mw.config.get('wgUserName');
            pageName = '';
            ruleStatus = {};
        }

        const currentNomidx = ACGATool.nominations.length;
        ACGATool.nominations.push({
            awarder: awarder, pageName: pageName, ruleStatus: ruleStatus
        });
        // console.log('generateNominationFieldset', ACGATool.nominations);

        // 得分者
        var userNameInput = new OO.ui.TextInputWidget({ value: ACGATool.nominations[currentNomidx].awarder });
        userNameInput.on('change', function (newValue) {
            ACGATool.nominations[currentNomidx].awarder = newValue;
        });
        var userNameField = new OO.ui.FieldLayout(userNameInput, {
            label: ACGATool.convByVar({ hant: '得分者', hans: '得分者' }),
            align: 'left'
        });

        // 條目名稱
        var pageNameInput = new OO.ui.TextInputWidget({ value: ACGATool.nominations[currentNomidx].pageName });
        pageNameInput.on('change', function (newValue) {
            ACGATool.nominations[currentNomidx].pageName = newValue;
        });
        var pageNameField = new OO.ui.FieldLayout(pageNameInput, {
            label: ACGATool.convByVar({ hant: '得分條目', hans: '得分条目' }),
            align: 'left',
            help: ACGATool.convByVar({ hant: '可填「他薦」', hans: '可填「他荐」' }),
            helpInline: true
        });

        // 提名理由
        var reasonFieldsetLayout = new OO.ui.FieldsetLayout();
        reasonFieldsetLayout.$element.css({
            'padding-top': '15px'  // 讓理由區域與上方的輸入框有點距離
        });

        var NominationRules = ACGATool.NominationRules();
        for (var i = 0; i < NominationRules.length; i++) {
            var ruleGroup = NominationRules[i];
            var ruleGroupFieldset = new OO.ui.FieldsetLayout({
                label: ruleGroup.group, align: 'top',
                help: ruleGroup.explanation, helpInline: true
            });

            if (ruleGroup.rules) {
                var ruleItems = [];
                for (var j = 0; j < ruleGroup.rules.length; j++) {
                    var rule = ruleGroup.rules[j];
                    var ruleCheckbox = new ACGATool.RuleCheckboxInputWidget({
                        value: rule.rule, ruleset: rule, nomidx: currentNomidx
                    });
                    ruleItems.push(ruleCheckbox);
                }
                var horizontalLayout = new OO.ui.HorizontalLayout({
                    items: ruleItems
                });
                ruleGroupFieldset.addItems(horizontalLayout);

            } else {
                // 處理 (5) 條目評審
                for (var j = 0; j < ruleGroup.tabs.length; j++) {
                    var tab = ruleGroup.tabs[j];
                    var tabFieldset = new OO.ui.FieldsetLayout({
                        label: tab.tab
                    });
                    tabFieldset.$element.find('legend > span').css({ 'font-size': '1.05em' });

                    var ruleItems = [];
                    for (var k = 0; k < tab.rules.length; k++) {
                        var rule = tab.rules[k];
                        var ruleCheckbox = new ACGATool.RuleCheckboxInputWidget({
                            value: rule.rule, ruleset: rule, nomidx: currentNomidx
                        });
                        ruleItems.push(ruleCheckbox);
                    }
                    var horizontalLayout = new OO.ui.HorizontalLayout({
                        items: ruleItems
                    });
                    tabFieldset.addItems(horizontalLayout);
                    ruleGroupFieldset.addItems([tabFieldset]);
                }
            }
            reasonFieldsetLayout.addItems([ruleGroupFieldset]);
        }

        var nominationFieldset = new OO.ui.FieldsetLayout({
            items: [userNameField, pageNameField, reasonFieldsetLayout]
        });
        nominationFieldset.$element.css({
            'margin-top': 0
        });

        if (!nomData) {
            // 頂部的 <hr>
            var hr = $('<hr>').css({
                'margin-top': '15px', 'margin-bottom': '15px'
            });
            nominationFieldset.$element.prepend(hr);
        }

        return nominationFieldset;
    },

    /**
     * 獲取XTools頁面資訊。無法獲取時按下不表,返回空字串。
     * @param pageName
     * @returns {Promise<string>} XTools頁面資訊。
     */
    getXToolsInfo: async function (pageName) {
        try {
            return await $.get('https://xtools.wmcloud.org/api/page/pageinfo/' + mw.config.get('wgServerName') + '/' + pageName.replace(/["?%&+\\]/g, escape) + '?format=html&uselang=' + mw.config.get('wgUserLanguage'));
        } catch (error) {
            console.error('Error fetching XTools data:', error);
            return '';
        }
    },

    /**
     * 生成提名檢查單。
     * @param nomData 提名資料。
     * @returns {OO.ui.FieldsetLayout} 提名檢查單。
     */
    generateChecklistFieldset: async function (nomData) {
        var awarder = nomData.awarder;
        var pageName = nomData.pageName;
        var ruleStatus = nomData.ruleStatus;
        ACGATool.nominations.push({
            awarder: awarder, pageName: pageName, ruleStatus: ruleStatus, invalid: false, message: ''
        });

        // 得分者
        var userNameLabelWidget = new OO.ui.LabelWidget({
            label: $('<a>').attr('href', mw.util.getUrl('User:' + awarder)).text(awarder),
        });
        var userTalkLabelWidget = new OO.ui.LabelWidget({
            label: $('<a>').attr('href', mw.util.getUrl('User talk:' + awarder)).text(ACGATool.convByVar({ hant: '討論', hans: '讨论' })),
        });
        var userContribsLabelWidget = new OO.ui.LabelWidget({
            label: $('<a>').attr('href', mw.util.getUrl('Special:用户贡献/' + awarder)).text(ACGATool.convByVar({ hant: '貢獻', hans: '贡献' })),
        });
        var userNameHorizontalLayout = new OO.ui.HorizontalLayout({
            items: [userNameLabelWidget, new OO.ui.LabelWidget({ label: '(' }), userTalkLabelWidget, new OO.ui.LabelWidget({ label: '·' }), userContribsLabelWidget, new OO.ui.LabelWidget({ label: ')' })]
        });
        userNameHorizontalLayout.$element.css({
            'gap': '4px', 'width': '80%', 'flex-shrink': '0', 'flex-wrap': 'wrap',
        });
        var userNameLabelWidget = new OO.ui.LabelWidget({
            label: ACGATool.convByVar({ hant: '得分者', hans: '得分者' }),
        });
        userNameLabelWidget.$element.css({
            'flex-grow': 1, 'align-self': 'stretch'
        });
        var userNameField = new OO.ui.HorizontalLayout({
            items: [userNameLabelWidget, userNameHorizontalLayout]
        });

        // 條目名稱
        var pageNameHorizontalLayout, pageNameLabelWidget;
        if (pageName === '他薦' || pageName === '他荐') {
            pageNameLabelWidget = new OO.ui.LabelWidget({
                label: ACGATool.convByVar({ hant: '他薦', hans: '他荐' }),
            });
            pageNameHorizontalLayout = new OO.ui.HorizontalLayout({
                items: [pageNameLabelWidget]
            });
        } else {
            pageNameLabelWidget = new OO.ui.LabelWidget({
                label: $('<a>').attr('href', mw.util.getUrl(pageName)).text(pageName),
            });
            var pageTalkLabelWidget = new OO.ui.LabelWidget({
                label: $('<a>').attr('href', mw.util.getUrl('Talk:' + pageName)).text(ACGATool.convByVar({ hant: '討論', hans: '讨论' })),
            });
            var pageHistoryLabelWidget = new OO.ui.LabelWidget({
                label: $('<a>').attr('href', mw.util.getUrl(pageName, { action: 'history' })).text(ACGATool.convByVar({ hant: '歷史', hans: '历史' })),
            });
            var pagesLinkedToPageLabelWidget = new OO.ui.LabelWidget({
                label: $('<a>').attr('href', mw.util.getUrl('Special:链入页面/' + pageName)).text(ACGATool.convByVar({ hant: '連入', hans: '链入' })),
            });
            // 更多頁面資訊 from XTools
            var xtoolsData = await ACGATool.getXToolsInfo(pageName);
            var xtoolsPageInfoLabelWidget = new OO.ui.LabelWidget({
                label: $('<div>').html(xtoolsData),
            });
            xtoolsPageInfoLabelWidget.$element.css({
                'font-size': '0.9em',
            });
            pageNameHorizontalLayout = new OO.ui.HorizontalLayout({
                items: [pageNameLabelWidget, new OO.ui.LabelWidget({ label: '(' }), pageTalkLabelWidget, new OO.ui.LabelWidget({ label: '·' }), pageHistoryLabelWidget, new OO.ui.LabelWidget({ label: '·' }), pagesLinkedToPageLabelWidget, new OO.ui.LabelWidget({ label: ')' }), xtoolsPageInfoLabelWidget]
            });
        }
        pageNameHorizontalLayout.$element.css({
            'gap': '4px', 'width': '80%', 'flex-shrink': '0', 'flex-wrap': 'wrap',
        });
        var pageLabelLabelWidget = new OO.ui.LabelWidget({
            label: ACGATool.convByVar({ hant: '得分條目', hans: '得分条目' }),
        });
        pageLabelLabelWidget.$element.css({
            'flex-grow': 1, 'align-self': 'stretch'
        });
        var pageNameField = new OO.ui.HorizontalLayout({
            items: [pageLabelLabelWidget, pageNameHorizontalLayout]
        });
        pageNameField.$element.css({
            'margin-top': '15px'
        });

        // 提名無效
        var invalidToggleSwitchWidget = new OO.ui.ToggleSwitchWidget({
            value: false,
        });
        invalidToggleSwitchWidget.on('change', function (isChecked) {
            ACGATool.nominations[0].invalid = isChecked;
        });
        var invalidField = new OO.ui.FieldLayout(invalidToggleSwitchWidget, {
            label: ACGATool.convByVar({ hant: '提名無效', hans: '提名无效' }),
            align: 'left'
        });
        invalidField.$element.css({
            'margin-top': '15px'
        });
        invalidField.$element.addClass('checklist-field');

        // 提名理由
        var reasonField = new OO.ui.HorizontalLayout();
        reasonField.$element.css({
            'margin-top': '15px'
        });
        var reasonLabelWidget = new OO.ui.LabelWidget({
            label: ACGATool.convByVar({ hant: '提名理由', hans: '提名理由' }),
        });
        reasonLabelWidget.$element.css({
            'flex-grow': 1, 'align-self': 'stretch'
        });

        var ruleItems = [];
        var ruleDict = ACGATool.NominationRuleSet().ruleDict;
        for (var ruleName in ruleStatus) {
            var rule = ruleDict[ruleName];
            var ruleCheckbox = new ACGATool.RuleCheckboxInputWidget({
                value: rule.rule, ruleset: rule, nomidx: 0, check: true
            });
            ruleItems.push(ruleCheckbox);
        }
        var horizontalLayout = new OO.ui.HorizontalLayout({
            items: ruleItems
        });
        horizontalLayout.$element.css({
            'width': '80%', 'flex-shrink': '0', 'flex-wrap': 'wrap',
        });
        reasonField.addItems([reasonLabelWidget, horizontalLayout]);

        // 附加說明
        var messageInput = new OO.ui.MultilineTextInputWidget({
            autosize: true, rows: 1
        });
        messageInput.on('change', function (newValue) {
            ACGATool.nominations[0].message = newValue;
        });
        messageInput.on('resize', function () {
            ACGATool.checkNominationDialog.updateSize();
        });
        var messageField = new OO.ui.FieldLayout(messageInput, {
            label: ACGATool.convByVar({ hant: '附加說明', hans: '附加说明' }),
            align: 'left',
            help: ACGATool.convByVar({ hant: '可不填;無須簽名', hans: '可不填;无须签名' }),
            helpInline: true
        });
        messageField.$element.css({
            'margin-top': '15px'
        });
        messageField.$element.addClass('checklist-field');

        var nominationFieldset = new OO.ui.FieldsetLayout({
            items: [userNameField, pageNameField, invalidField, reasonField, messageField]
        });
        nominationFieldset.$element.css({
            'margin-top': 0
        });
        return nominationFieldset;
    },

    /**
     * 生成提名理由。
     * @param ruleStatus
     * @param check
     * @returns {{reasonText: string, unselectedReasonText: string}|string|null}
     */
    generateReason: function (ruleStatus, check) {
        // 拼湊提名理由
        var reasonText = '', unselectedReasonText = '';
        var reasonScore = 0;
        for (var rule in ruleStatus) {
            if (check ? true : ruleStatus[rule].selected) {
                if (isNaN(ruleStatus[rule].score) || ruleStatus[rule].score < 0) {
                    mw.notify(
                        ACGATool.convByVar({ hant: '規則', hans: '规则' }) + '「' + rule + '」' + ACGATool.convByVar({ hant: '的分數不合法,請檢查!', hans: '的分数不合法,请检查!' }),
                        { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                    );
                    return null;
                }
                // if (ruleStatus[rule].score > ruleStatus[rule].maxScore) {
                //     mw.notify(
                //         ACGATool.convByVar({ hant: '規則', hans: '规则' }) + '「' + rule + '」' + ACGATool.convByVar({ hant: '的分數超過最大值', hans: '的分数超过最大值' }) + '「' + ruleStatus[rule].maxScore + '」' + ACGATool.convByVar({ hant: ',請檢查!', hans: ',请检查!' }),
                //         { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                //     );
                //     return null;
                // }

                if (ruleStatus[rule].selected) {
                    reasonText += ' ' + rule;
                    if (ruleStatus[rule].desc !== ruleStatus[rule].ogDesc) {
                        // console.log('desc', ruleStatus[rule].desc, ruleStatus[rule].ogDesc);
                        reasonText += '(' + ruleStatus[rule].desc + ')';
                    }
                    if (ruleStatus[rule].score !== ruleStatus[rule].maxScore) {
                        reasonText += '[' + ruleStatus[rule].score + ']';
                    }
                    reasonScore += ruleStatus[rule].score;
                } else if (check) {
                    unselectedReasonText += ' ' + rule;
                    if (ruleStatus[rule].desc !== ruleStatus[rule].ogDesc) {
                        // console.log('desc', ruleStatus[rule].desc, ruleStatus[rule].ogDesc);
                        unselectedReasonText += '(' + ruleStatus[rule].desc + ')';
                    }
                    if (ruleStatus[rule].score !== ruleStatus[rule].maxScore) {
                        unselectedReasonText += '[' + ruleStatus[rule].score + ']';
                    }
                }
            }
        }
        reasonText = reasonText.trim();
        unselectedReasonText = unselectedReasonText.trim();
        if (check) {
            return {
                reasonText: reasonText,
                unselectedReasonText: unselectedReasonText,
                reasonScore: reasonScore
            };
        }
        return reasonText;
    },

    /**
     * 保存新提名。
     * @returns {Promise<boolean>} 是否成功提交。
     */
    saveNewNomination: async function () {
        var proposedWikitext = '{{ACG提名2';

        for (var i = 0; i < ACGATool.nominations.length; i++) {
            var nomination = ACGATool.nominations[i];
            if (nomination.awarder === '' || nomination.pageName === '') {
                mw.notify(
                    ACGATool.convByVar({ hant: '得分者或得分條目未填寫,請檢查!', hans: '得分者或得分条目未填写,请检查!' }),
                    { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                );
                return true;
            }

            var reasonText = ACGATool.generateReason(nomination.ruleStatus);
            if (reasonText == null) {
                return true;
            }
            if (reasonText === '') {
                mw.notify(
                    ACGATool.convByVar({ hant: '未選擇任何評審規則,請檢查!', hans: '未选择任何评审规则,请检查!' }),
                    { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                );
                return true;
            }

            proposedWikitext += '\n|條目名稱' + (i + 1) + ' = ' + nomination.pageName.trim();
            proposedWikitext += '\n|用戶名稱' + (i + 1) + ' = ' + nomination.awarder.trim();
            proposedWikitext += '\n|提名理由' + (i + 1) + ' = {{ACG提名2/request|ver=1|' + reasonText + '}}';
            proposedWikitext += '\n|核對用' + (i + 1) + ' = {{ACG提名2/check|ver=1|}}';
        }

        const signature = '~' + '~' + '~' + '~';
        proposedWikitext += "\n}}\n'''提名人:'''" + signature;

        // 附加說明
        var message = ACGATool.newNominationDialog.messageInput.getValue().trim();
        if (message !== '') {
            proposedWikitext += "\n: {{說明}}:" + message + '--' + signature;
        }

        // 是否已有今日的date
        var today = new Date();
        var todayDate = (today.getMonth() + 1) + '月' + today.getDate() + '日';
        var fulltext = await ACGATool.getFullText();
        if (!fulltext.includes('=== ' + todayDate + ' ===')) {
            // 沒有今日的date,先新增一個
            proposedWikitext = '=== ' + todayDate + ' ===\n' + proposedWikitext;
        }

        // 提交
        var api = new mw.Api();
        var response = await api.postWithToken('csrf', {
            action: 'edit', title: 'WikiProject:ACG/維基ACG專題獎/登記處', appendtext: '\n' + proposedWikitext, summary: '[[User:SuperGrey/gadgets/ACGATool|新提名]]',
        });
        if (response.edit.result === 'Success') {
            mw.notify(
                ACGATool.convByVar({ hant: '新提名已成功提交!', hans: '新提名已成功提交!' }),
                { title: ACGATool.convByVar({ hant: '成功', hans: '成功' }), autoHide: true }
            );
            ACGATool.refreshPage();
            return false;
        } else {
            mw.notify(
                ACGATool.convByVar({ hant: '新提名提交失敗:', hans: '新提名提交失败:' }) + response.edit.result,
                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
            );
            return true;
        }
    },

    /**
     * 保存修改提名。
     * @returns {Promise<boolean>} 是否成功提交。
     */
    saveModifiedNomination: async function () {
        var nomination = ACGATool.nominations[0];
        if (nomination.awarder === '' || nomination.pageName === '') {
            mw.notify(
                ACGATool.convByVar({ hant: '得分者或得分條目未填寫,請檢查!', hans: '得分者或得分条目未填写,请检查!' }),
                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
            );
            return true;
        }

        var reasonText = ACGATool.generateReason(nomination.ruleStatus);
        if (reasonText == null) {
            return true;
        }
        if (reasonText === '') {
            mw.notify(
                ACGATool.convByVar({ hant: '未選擇任何評審規則,請檢查!', hans: '未选择任何评审规则,请检查!' }),
                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
            );
            return true;
        }

        var fulltext = await ACGATool.getFullText(), updatedText;
        if (ACGATool.queried.type === 'main' || ACGATool.queried.type === 'extra') {
            var changes = {
                '條目名稱': nomination.pageName, '用戶名稱': nomination.awarder, '提名理由': '{{ACG提名2/request|ver=1|' + reasonText + '}}'
            };
            updatedText = ACGATool.updateEntryParameters(fulltext, ACGATool.queried, changes);
        } else if (ACGATool.queried.type === 'acg2') {
            var changes = {
                '條目名稱': nomination.pageName, '用戶名稱': nomination.awarder, '提名理由': '{{ACG提名2/request|ver=1|' + reasonText + '}}'
            };
            updatedText = ACGATool.updateEntryParameters(fulltext, ACGATool.queried, changes);
        }
        if (updatedText === fulltext) {
            mw.notify(
                ACGATool.convByVar({ hant: '提名並未改動!', hans: '提名并未改动!' }),
                { type: 'warn', title: ACGATool.convByVar({ hant: '提示', hans: '提示' }) }
            );
            return true;
        }

        var api = new mw.Api();
        var response = await api.postWithToken('csrf', {
            action: 'edit', title: 'WikiProject:ACG/維基ACG專題獎/登記處', text: updatedText, summary: '[[User:SuperGrey/gadgets/ACGATool|編輯提名]]',
        });
        if (response.edit.result === 'Success') {
            mw.notify(
                ACGATool.convByVar({ hant: '提名已成功修改!', hans: '提名已成功修改!' }),
                { title: ACGATool.convByVar({ hant: '成功', hans: '成功' }), autoHide: true }
            );
            ACGATool.refreshPage();
            return false;
        } else {
            mw.notify(
                ACGATool.convByVar({ hant: '提名修改失敗:', hans: '提名修改失败:' }) + response.edit.result,
                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
            );
            return true;
        }
    },

    /**
     * 保存核分。
     * @returns {Promise<boolean>} 是否成功提交。
     */
    saveNominationCheck: async function () {
        var nomination = ACGATool.nominations[0];
        // console.log('nomination', nomination);
        var checkText = '{{ACG提名2/check|ver=1|';

        // 是否選擇「提名無效」
        if (nomination.invalid) {
            checkText += '0';
            var reasonScore = 0;
        } else {
            var reasonObject = ACGATool.generateReason(nomination.ruleStatus, true);
            if (reasonObject == null) {
                return true;
            }
            var reasonText = reasonObject.reasonText;
            var unselectedReasonText = reasonObject.unselectedReasonText;
            var reasonScore = reasonObject.reasonScore;
            checkText += reasonText;
            if (unselectedReasonText !== '') {
                checkText += '|no=' + unselectedReasonText;
            }
        }
        checkText += '}}' + nomination.message;
        var signature = '~' + '~' + '~' + '~';
        checkText += '--' + signature;

        // console.log(checkText);

        var fulltext = await ACGATool.getFullText(), updatedText;
        if (ACGATool.queried.type === 'main' || ACGATool.queried.type === 'extra') {
            var changes = {
                '核對用': checkText
            };
            updatedText = ACGATool.updateEntryParameters(fulltext, ACGATool.queried, changes);
        } else if (ACGATool.queried.type === 'acg2') {
            var changes = {
                '核對用': checkText
            };
            updatedText = ACGATool.updateEntryParameters(fulltext, ACGATool.queried, changes);
        }
        if (updatedText === fulltext) {
            mw.notify(
                ACGATool.convByVar({ hant: '核分並未改動!', hans: '核分并未改动!' }),
                { type: 'warn', title: ACGATool.convByVar({ hant: '提示', hans: '提示' }) }
            );
            return true;
        }

        var api = new mw.Api();
        var response = await api.postWithToken('csrf', {
            action: 'edit', title: 'WikiProject:ACG/維基ACG專題獎/登記處', text: updatedText, summary: '[[User:SuperGrey/gadgets/ACGATool|核對分數]]',
        });
        if (response.edit.result === 'Success') {
            mw.notify(
                ACGATool.convByVar({ hant: '核分已成功提交!即將自動打開 Module:ACGaward/list,請在其中手動更新得分排行榜。', hans: '核分已成功提交!即将自动打开 Module:ACGaward/list,请在其中手动更新得分排行榜。' }),
                { title: ACGATool.convByVar({ hant: '成功', hans: '成功' }), autoHide: false }
            );
            // 立即打開 Module:ACGaward/list
            if (reasonScore > 0) {
                window.open(mw.util.getUrl('Module:ACGaward/list') + '?action=edit&summary=' + nomination.awarder + "+X%2B" + reasonScore.toString() + "%3DY");
            }
            // 2秒後刷新頁面
            ACGATool.refreshPage();
            return false;
        } else {
            mw.notify(
                ACGATool.convByVar({ hant: '核分提交失敗:', hans: '核分提交失败:' }) + response.edit.result,
                { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
            );
            return true;
        }
    },

    /**
     * 刷新頁面。
     */
    refreshPage: function () {
        // 2秒後刷新頁面
        setTimeout(function () {
            location.reload();
        }, 2000);
    },

    /**
     * 新提名對話框。
     * @param config
     * @constructor
     */
    NewNominationDialog: function (config) {
        ACGATool.NewNominationDialog.super.call(this, config);
    },

    /**
     * 初始化新提名對話框。在腳本啟動時執行一次。
     */
    initNewNominationDialog: function () {
        OO.inheritClass(ACGATool.NewNominationDialog, OO.ui.ProcessDialog);

        ACGATool.NewNominationDialog.static.name = 'NewNominationDialog';
        ACGATool.NewNominationDialog.static.title = ACGATool.convByVar({ hant: '新提名(維基ACG專題獎小工具)', hans: '新提名(维基ACG专题奖小工具)' });
        ACGATool.NewNominationDialog.static.actions = [
            {
                action: 'save',
                label: ACGATool.convByVar({ hant: '儲存', hans: '储存' }),
                flags: ['primary', 'progressive']
            },
            {
                action: 'cancel',
                label: ACGATool.convByVar({ hant: '取消', hans: '取消' }),
                flags: 'safe'
            },
            {
                action: 'add',
                label: ACGATool.convByVar({ hant: '額外提名 + 1', hans: '额外提名 + 1' })
            },
            {
                action: 'minus',
                label: ACGATool.convByVar({ hant: '額外提名 − 1', hans: '额外提名 − 1' })
            }
        ];
        ACGATool.NewNominationDialog.prototype.initialize = function () {
            ACGATool.NewNominationDialog.super.prototype.initialize.call(this);
            this.panel = new OO.ui.PanelLayout({
                padded: true, expanded: false
            });
            this.content = new OO.ui.FieldsetLayout();

            // 附加說明
            this.messageInput = new OO.ui.MultilineTextInputWidget({
                autosize: true, rows: 1
            });
            this.messageInput.connect(this, { resize: 'onMessageInputResize' });
            this.messageInputField = new OO.ui.FieldLayout(this.messageInput, {
                label: ACGATool.convByVar({ hant: '附加說明', hans: '附加说明' }),
                align: 'top',
                help: ACGATool.convByVar({ hant: '可不填;無須簽名', hans: '可不填;无须签名' }),
                helpInline: true
            });
            this.messageInputFieldSet = new OO.ui.FieldsetLayout({
                items: [this.messageInputField]
            });

            this.content.addItems([this.messageInputFieldSet, ACGATool.generateNominationFieldset()]);

            this.panel.$element.append(this.content.$element);
            this.$body.append(this.panel.$element);
        };

        ACGATool.NewNominationDialog.prototype.onMessageInputResize = function () {
            this.updateSize();
        };

        ACGATool.NewNominationDialog.prototype.getBodyHeight = function () {
            return this.panel.$element.outerHeight(true);
        };

        ACGATool.NewNominationDialog.prototype.getActionProcess = function (action) {
            if (action === 'save') {
                return new OO.ui.Process(async function () {
                    let response = await ACGATool.saveNewNomination();
                    if (!response) {
                        this.close();
                    }
                }, this);
            } else if (action === 'add') {
                return new OO.ui.Process(function () {
                    // 新增一個提名
                    var newFieldset = ACGATool.generateNominationFieldset();
                    this.content.addItems([newFieldset]);
                    this.updateSize();
                }, this);
            } else if (action === 'minus') {
                return new OO.ui.Process(function () {
                    if (this.content.items.length <= 2) {
                        mw.notify(
                            ACGATool.convByVar({ hant: '至少需要一個提名!', hans: '至少需要一个提名!' }),
                            { type: 'error', title: ACGATool.convByVar({ hant: '錯誤', hans: '错误' }) }
                        );
                        return;
                    }
                    // 移除最後一個提名
                    this.content.removeItems([this.content.items[this.content.items.length - 1]]);
                    ACGATool.nominations.pop();
                    this.updateSize();
                }, this);
            } else if (action === 'cancel') {
                return new OO.ui.Process(function () {
                    this.close();
                }, this);
            }
            return ACGATool.NewNominationDialog.super.prototype.getActionProcess.call(this, action);
        };

        ACGATool.NewNominationDialog.prototype.getTearnDownProcess = function (data) {
            return ACGATool.NewNominationDialog.super.prototype.getTearnDownProcess.call(this, data);
        };
    },

    /**
     * 顯示新提名對話框。
     */
    showNewNominationDialog: function () {
        // 清空原有的items
        ACGATool.nominations.length = 0;

        ACGATool.newNominationDialog = new ACGATool.NewNominationDialog({ size: 'large', padded: true, scrollable: true });
        ACGATool.windowManager.addWindows([ACGATool.newNominationDialog]);
        ACGATool.windowManager.openWindow(ACGATool.newNominationDialog);
    },

    /**
     * 編輯提名對話框。
     * @param config
     * @constructor
     */
    EditNominationDialog: function (config) {
        ACGATool.EditNominationDialog.super.call(this, config);
    },

    /**
     * 初始化編輯提名對話框。在腳本啟動時執行一次。
     */
    initEditNominationDialog: function () {
        OO.inheritClass(ACGATool.EditNominationDialog, OO.ui.ProcessDialog);

        ACGATool.EditNominationDialog.static.name = 'EditNominationDialog';
        ACGATool.EditNominationDialog.static.title = ACGATool.convByVar({ hant: '編輯提名(維基ACG專題獎小工具)', hans: '编辑提名(维基ACG专题奖小工具)' });
        ACGATool.EditNominationDialog.static.actions = [
            {
                action: 'save',
                label: ACGATool.convByVar({ hant: '儲存', hans: '储存' }),
                flags: ['primary', 'progressive']
            },
            {
                action: 'cancel',
                label: ACGATool.convByVar({ hant: '取消', hans: '取消' }),
                flags: 'safe'
            }
        ];
        ACGATool.EditNominationDialog.prototype.initialize = function () {
            ACGATool.EditNominationDialog.super.prototype.initialize.call(this);
            this.panel = new OO.ui.PanelLayout({
                padded: true, expanded: false
            });
            this.panel.$element.append(ACGATool.editNominationDialogContent.$element);
            this.$body.append(this.panel.$element);
        };

        ACGATool.EditNominationDialog.prototype.onMessageInputResize = function () {
            this.updateSize();
        };

        ACGATool.EditNominationDialog.prototype.getBodyHeight = function () {
            return this.panel.$element.outerHeight(true);
        };

        ACGATool.EditNominationDialog.prototype.getActionProcess = function (action) {
            if (action === 'save') {
                return new OO.ui.Process(async function () {
                    let response = await ACGATool.saveModifiedNomination();
                    if (!response) {
                        this.close();
                    }
                }, this);
            } else if (action === 'cancel') {
                return new OO.ui.Process(function () {
                    this.close();
                }, this);
            }
            return ACGATool.EditNominationDialog.super.prototype.getActionProcess.call(this, action);
        };

        ACGATool.EditNominationDialog.prototype.getTearnDownProcess = function (data) {
            return ACGATool.EditNominationDialog.super.prototype.getTearnDownProcess.call(this, data);
        };
    },

    /**
     * 顯示編輯提名對話框。
     * @param nomData 提名資料。
     */
    showEditNominationDialog: function (nomData) {
        // 清空原有的items
        ACGATool.editNominationDialogContent.clearItems();
        ACGATool.nominations.length = 0;

        ACGATool.editNominationDialog = new ACGATool.EditNominationDialog({ size: 'large', padded: true, scrollable: true });

        // 加入新的items
        ACGATool.editNominationDialogContent.addItems([ACGATool.generateNominationFieldset(nomData)]);

        ACGATool.windowManager.addWindows([ACGATool.editNominationDialog]);
        ACGATool.windowManager.openWindow(ACGATool.editNominationDialog);
    },

    /**
     * 核分提名對話框。
     * @param config
     * @constructor
     */
    CheckNominationDialog: function (config) {
        ACGATool.CheckNominationDialog.super.call(this, config);
    },

    /**
     * 初始化核分提名對話框。在腳本啟動時執行一次。
     */
    initCheckNominationDialog: function () {
        OO.inheritClass(ACGATool.CheckNominationDialog, OO.ui.ProcessDialog);

        ACGATool.CheckNominationDialog.static.name = 'CheckNominationDialog';
        ACGATool.CheckNominationDialog.static.title = ACGATool.convByVar({ hant: '核對分數(維基ACG專題獎小工具)', hans: '核对分数(维基ACG专题奖小工具)' });
        ACGATool.CheckNominationDialog.static.actions = [
            {
                action: 'save',
                label: ACGATool.convByVar({ hant: '儲存', hans: '储存' }),
                flags: ['primary', 'progressive']
            },
            {
                action: 'cancel',
                label: ACGATool.convByVar({ hant: '取消', hans: '取消' }),
                flags: 'safe'
            }
        ];
        ACGATool.CheckNominationDialog.prototype.initialize = function () {
            ACGATool.CheckNominationDialog.super.prototype.initialize.call(this);
            this.panel = new OO.ui.PanelLayout({
                padded: true, expanded: false
            });
            this.panel.$element.append(ACGATool.checkNominationDialogContent.$element);
            this.$body.append(this.panel.$element);
        };

        ACGATool.CheckNominationDialog.prototype.onMessageInputResize = function () {
            this.updateSize();
        };

        ACGATool.CheckNominationDialog.prototype.getBodyHeight = function () {
            return this.panel.$element.outerHeight(true);
        };

        ACGATool.CheckNominationDialog.prototype.getActionProcess = function (action) {
            if (action === 'save') {
                return new OO.ui.Process(async function () {
                    let response = await ACGATool.saveNominationCheck();
                    if (!response) {
                        this.close();
                    }
                }, this);
            } else if (action === 'cancel') {
                return new OO.ui.Process(function () {
                    this.close();
                }, this);
            }
            return ACGATool.CheckNominationDialog.super.prototype.getActionProcess.call(this, action);
        };

        ACGATool.CheckNominationDialog.prototype.getTearnDownProcess = function (data) {
            return ACGATool.CheckNominationDialog.super.prototype.getTearnDownProcess.call(this, data);
        };

        mw.util.addCSS('.checklist-field .oo-ui-fieldLayout-field { width: 80% !important; }');
    },

    /**
     * 顯示核分提名對話框。
     * @param nomData 提名資料。
     */
    showCheckNominationDialog: function (nomData) {
        // 清空原有的items
        ACGATool.checkNominationDialogContent.clearItems();
        ACGATool.nominations.length = 0;

        ACGATool.checkNominationDialog = new ACGATool.CheckNominationDialog({ size: 'large', padded: true, scrollable: true });

        // 加入新的items
        ACGATool.generateChecklistFieldset(nomData).then(function (field) {
            ACGATool.checkNominationDialogContent.addItems([field]);

            ACGATool.windowManager.addWindows([ACGATool.checkNominationDialog]);
            ACGATool.windowManager.openWindow(ACGATool.checkNominationDialog);
        });
    },

    /**
     * 腳本入口。
     */
    init: function () {
        ACGATool.pageName = mw.config.get('wgPageName');
        if (ACGATool.pageName !== 'WikiProject:ACG/維基ACG專題獎' && ACGATool.pageName !== 'WikiProject:ACG/維基ACG專題獎/登記處') {
            // 非目標頁面,不執行
            return;
        }

        mw.loader.using('ext.gadget.HanAssist').then((require) => {
            const { convByVar } = require('ext.gadget.HanAssist');
            ACGATool.convByVar = convByVar;

            // Initialize OOUI custom widgets
            ACGATool.initDynamicWidthTextInputWidget();
            ACGATool.initRuleCheckboxInputWidget();
            // Initialize dialogs
            ACGATool.initNewNominationDialog();
            ACGATool.initEditNominationDialog();
            ACGATool.initCheckNominationDialog();

            // Append the window manager element to the body
            ACGATool.windowManager = new OO.ui.WindowManager();
            $(document.body).append(ACGATool.windowManager.$element);

            // 添加提名按鈕
            ACGATool.addEditButtonsToPage();
        });
    },

    pageName: '',  // JS運行的當前頁面
    windowManager: null,  // Window manager for OOUI dialogs
    newNominationDialog: null,  // 新提名dialog
    editNominationDialog: null,  // 修改提名dialog
    editNominationDialogContent: new OO.ui.FieldsetLayout(), // 修改提名dialog的內容池
    checkNominationDialog: null,  // 檢查提名dialog
    checkNominationDialogContent: new OO.ui.FieldsetLayout(), // 檢查提名dialog的內容池
    queried: null,  // 查詢到的提名
    nominations: [],  // 提名
    convByVar: null  // 簡繁轉換
};

ACGATool.init();