跳转到内容

User:SuperGrey/gadgets/RedirectHelper/main.js

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

// <nowiki>
// Core configuration and utilities
const CONFIG = {
    STYLES: `
        #create-redirect-button { margin-bottom: 20px }
        #redirect-helper-box {
            margin-right: auto;
            margin-bottom: 25px !important;
            margin-left: auto;
            background-color: #f5f5f5;
            width: 700px;
            max-width: calc(100% - 50px);
            color: #202122;
        }
        .redirect-input-layout label { font-weight: 700 }
        .redirect-helper-redirect-possibilities:after {
            content: " (redirect with possibilities)";
            font-style: italic;
        }
        .redirect-helper-template-parameters-container,
        .redirect-helper-template-parameters-container details {
            margin-top: 10px;
            margin-bottom: 10px;
            border-radius: 5px;
            background-color: #e2e2e2;
            padding: 5px;
        }
        .redirect-helper-template-parameters-container summary {
            cursor: pointer;
            font-weight: 700;
        }
        .redirect-helper-template-parameters-container details {
            margin-top: 5px;
            margin-bottom: 5px;
            background-color: #d1cece;
        }
        #redirect-helper-no-templates-message { padding: 5px }
        #redirect-helper-summary-layout {
            margin-top: 15px;
            border-top: 1px solid gray;
            padding-top: 10px;
        }
        #redirect-helper-submit-layout { margin-top: 10px }
        #redirect-helper-submit-layout > * { margin-bottom: 0 }
        .redirect-helper-warning { margin-top: 8px }
        .redirect-helper-autofix-button {
            margin-left: 5px;
            font-size: 12px;
        }
        .redirect-helper-autofix-button a {
            padding: 3px 4px !important;
            min-height: 0 !important;
            min-height: initial !important;
        }
    `,
    REQUIRED_MODULES: [
        'mediawiki.util',
        'oojs-ui-core',
        'oojs-ui-widgets',
        'oojs-ui-windows',
        'oojs-ui.styles.icons-content',
        'oojs-ui.styles.icons-editing-core',
        'oojs-ui.styles.icons-interactions',
        'oojs-ui.styles.icons-layout',
        'oojs-ui.styles.icons-moderation',
        'oojs-ui.styles.icons-editing-core'
    ],
    SCRIPT_MESSAGE: ' ([[User:SuperGrey/gadgets/RedirectHelper|RedirectHelper]])'
};

// Utility functions
const Utils = {
    async fetchRedirectTemplates(api) {
        return JSON.parse(
            (
                await api.get({
                    action: "query",
                    formatversion: "2",
                    prop: "revisions",
                    rvprop: "content",
                    rvslots: "main",
                    titles: "User:SuperGrey/gadgets/RedirectHelper/categories.json",
                })
            ).query.pages[0]?.revisions?.[0]?.slots?.main?.content || "{}",
        );
    },

    async getPageContent(api, title) {
        const response = await api.get({
            action: 'query',
            formatversion: '2',
            prop: 'revisions',
            rvprop: 'content',
            rvslots: 'main',
            titles: title
        });
        return response.query.pages[0].revisions[0].slots.main.content.trim();
    },

    async editOrCreate(api, title, content, summary, watchlist = 'preferences') {
        try {
            return await api.edit(title, () => ({ text: content, summary }));
        } catch (error) {
            if (error === 'nocreate-missing') {
                return await api.create(title, { summary, watchlist }, content);
            }
            mw.notify(`Error editing or creating ${title}: ${error?.error?.info ?? 'Unknown error'} (${error})`, { type: 'error' });
            return null;
        }
    }
};

// Custom input widgets
class RedirectInputWidget extends OO.ui.TextInputWidget {
    constructor(config, pageTitleParsed) {
        super(config);
        OO.ui.mixin.LookupElement.call(this, config);
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        this.pageTitleParsed = pageTitleParsed;
    }
}
OO.mixinClass(RedirectInputWidget, OO.ui.mixin.LookupElement);

RedirectInputWidget.prototype.getLookupRequest = function () {
    return this.api.get({
        action: 'query',
        formatversion: '2',
        list: 'search',
        srsearch: this.getValue(),
        srnamespace: '*',
        srlimit: 10
    });
};

RedirectInputWidget.prototype.getLookupMenuOptionsFromData = function (data) {
    const list = data?.query?.search ?? [];
    return list.map(result => {
        const title = mw.Title.newFromText(result.title);
        return new OO.ui.MenuOptionWidget({
            data: title.getPrefixedText(),
            label: title.getPrefixedText()
        });
    });
};

RedirectInputWidget.prototype.getLookupCacheDataFromResponse = function (data) {
    return data?.query?.search ?? [];
};

RedirectInputWidget.prototype.getLookupCacheListFromData = function (cacheData) {
    return cacheData.map(result => {
        const title = mw.Title.newFromText(result.title);
        return new OO.ui.MenuOptionWidget({
            data: title.getPrefixedText(),
            label: title.getPrefixedText()
        });
    });
};

class CategoryInputWidget extends OO.ui.TextInputWidget {
    constructor(config) {
        super(config);
        OO.ui.mixin.LookupElement.call(this, config);
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        this.validCategories = new Set();
    }
}
OO.mixinClass(CategoryInputWidget, OO.ui.mixin.LookupElement);

CategoryInputWidget.prototype.getLookupRequest = function () {
    return this.api.get({
        action: 'query',
        formatversion: '2',
        list: 'search',
        srsearch: `Category:${this.getValue()}`,
        srnamespace: 14,
        srlimit: 10
    });
};

CategoryInputWidget.prototype.getLookupMenuOptionsFromData = function (data) {
    return data.query.search.map(result => {
        const title = mw.Title.newFromText(result.title);
        const name = title.getName();
        this.validCategories.add(name);
        return new OO.ui.MenuOptionWidget({
            data: name,
            label: name
        });
    });
};

// Dialog classes
class OutputPreviewDialog extends OO.ui.ProcessDialog {
    constructor(config, pageTitleParsed) {
        super(config);
        this.pageTitleParsed = pageTitleParsed;
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        OutputPreviewDialog.static.name = 'OutputPreviewDialog';
        OutputPreviewDialog.static.title = 'Redirect categorization templates preview';
        OutputPreviewDialog.static.actions = [
            {
                action: 'cancel',
                label: 'Close',
                flags: ['safe', 'close']
            }
        ];
    }

    getSetupProcess() {
        return super.getSetupProcess().next(() => {
            return this.api.post({
                action: 'parse',
                formatversion: '2',
                contentmodel: 'wikitext',
                prop: ['text', 'categorieshtml'],
                title: this.pageTitleParsed.getPrefixedDb(),
                text: this.getData()
            }).then(data => {
                const text = data.parse.text;
                const categoriesHtml = data.parse.categorieshtml;
                const panel = new OO.ui.PanelLayout({
                    padded: true,
                    expanded: false
                });
                panel.$element.append(text, categoriesHtml);
                this.$body.append(panel.$element);
            });
        });
    }

    getActionProcess(action) {
        if (action) {
            return new OO.ui.Process(() => {
                this.close();
            });
        }
        return super.getActionProcess(action);
    }

    getTeardownProcess() {
        return super.getTeardownProcess().next(() => {
            this.$body.empty();
        });
    }
}
// Object.assign(OutputPreviewDialog.prototype, OO.ui.ProcessDialog.prototype);

class ChangesDialog extends OO.ui.ProcessDialog {
    constructor(config) {
        super(config);
        this.hasLoadedDiffStyles = false;
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        ChangesDialog.static.name = 'ChangesDialog';
        ChangesDialog.static.title = '編輯預覽';
        ChangesDialog.static.actions = [
            {
                action: 'cancel',
                label: 'Close',
                flags: ['safe', 'close']
            }
        ];
    }

    getSetupProcess() {
        return super.getSetupProcess().next(() => {
            if (!this.hasLoadedDiffStyles) {
                mw.loader.addLinkTag('https://www.mediawiki.org/w/load.php?modules=mediawiki.diff.styles&only=styles');
                this.hasLoadedDiffStyles = true;
            }

            const [oldText, newText] = this.getData();
            return this.api.post({
                action: 'compare',
                formatversion: '2',
                prop: ['diff'],
                fromslots: 'main',
                'fromtext-main': oldText,
                'fromcontentmodel-main': 'wikitext',
                toslots: 'main',
                'totext-main': newText,
                'tocontentmodel-main': 'wikitext'
            }).then(data => {
                const diffBody = data.compare.body;
                const noChangesMessage = new OO.ui.MessageWidget({
                    type: 'warning',
                    label: '沒有變化!'
                });
                const panel = new OO.ui.PanelLayout({
                    padded: true,
                    expanded: false
                });

                if (diffBody) {
                    panel.$element.append(`
  <table class="diff diff-editfont-monospace">
      <colgroup>
          <col class="diff-marker">
          <col class="diff-content">
          <col class="diff-marker">
          <col class="diff-content">
      </colgroup>
      <tbody>
								${diffBody}
      </tbody>
						</table>
					`);
                } else {
                    panel.$element.append(noChangesMessage.$element[0]);
                }
                this.$body.append(panel.$element);
            });
        });
    }

    getActionProcess(action) {
        if (action) {
            return new OO.ui.Process(() => {
                this.close();
            });
        }
        return super.getActionProcess(action);
    }

    getTeardownProcess() {
        return super.getTeardownProcess().next(() => {
            this.$body.empty();
        });
    }
}
// Object.assign(ChangesDialog.prototype, OO.ui.ProcessDialog.prototype);

// Validator class
class RedirectValidator {
    constructor(api) {
        this.api = api;
    }

    async validateRedirectTarget(target, sourceTitle) {
        if (!target) {
            return {
                valid: false,
                message: '請輸入重定向目標!'
            };
        }

        const targetParsed = mw.Title.newFromText(target);
        if (!targetParsed) {
            return {
                valid: false,
                message: '無效的重定向目標!'
            };
        }

        if (targetParsed.getPrefixedText() === sourceTitle.getPrefixedText()) {
            return {
                valid: false,
                message: '不能重定向到自身!'
            };
        }

        // Check if target is a redirect
        const targetInfo = await this.api.get({
            action: 'query',
            formatversion: '2',
            prop: ['info', 'pageprops'],
            titles: targetParsed.getPrefixedText()
        });

        const targetPage = targetInfo.query.pages[0];

        if (targetPage.missing) {
            return {
                valid: false,
                message: '重定向目標不存在!'
            };
        }

        if (targetPage.redirect) {
            return {
                valid: true,
                warning: '重定向目標已經是重定向',
                message: '重定向目標已經是重定向。是否自動修復為此重定向的最終目標?'
            };
        }

        if (targetPage.pageprops?.disambiguation) {
            return {
                valid: true,
                warning: '重定向目標是消歧義頁面',
                message: '重定向目標是消歧義頁面。確定要重定向到此頁面嗎?'
            };
        }

        return {
            valid: true
        };
    }

    validateTemplateParameters(templates, parameters) {
        const errors = [];

        Object.entries(templates).forEach(([tag, template]) => {
            if (!template.parameters) return;

            const templateParams = parameters[tag] || {};
            const requiredParams = Object.entries(template.parameters)
                .filter(([_, config]) => config.required)
                .map(([name]) => name);

            requiredParams.forEach(param => {
                if (!templateParams[param]) {
                    errors.push(`模板「${template.label}」缺少必填參數「${param}」`);
                }
            });
        });

        return {
            valid: errors.length === 0,
            errors
        };
    }

    validateSummary(summary) {
        if (!summary) {
            return {
                valid: false,
                message: '請輸入編輯摘要!'
            };
        }

        return {
            valid: true
        };
    }

    async validateAll(target, sourceTitle, templates, parameters, categories, summary) {
        const targetValidation = await this.validateRedirectTarget(target, sourceTitle);
        if (!targetValidation.valid) {
            return targetValidation;
        }

        const templateValidation = this.validateTemplateParameters(templates, parameters);
        if (!templateValidation.valid) {
            return {
                valid: false,
                message: templateValidation.errors.join('\n')
            };
        }

        const summaryValidation = this.validateSummary(summary);
        if (!summaryValidation.valid) {
            return summaryValidation;
        }

        return {
            valid: true,
            warning: targetValidation.warning,
            message: targetValidation.message
        };
    }
}

// Editor UI class
class RedirectEditorUI {
    constructor(editor) {
        this.editor = editor;
        this.api = editor.api;
        this.validator = new RedirectValidator(this.api);
    }

    createEditorBox() {
        return new OO.ui.PanelLayout({
            id: 'redirect-helper-box',
            padded: true,
            expanded: false,
            framed: true
        });
    }

    createRedirectInput() {
        const input = new RedirectInputWidget({
            placeholder: '目標頁面名稱',
            required: true
        }, this.editor.pageTitleParsed);

        input.on('blur', () => {
            const target = input.getValue().trim();
            if (!target) return;

            // Re‑use your existing validator
            this.validator.validateRedirectTarget(target, this.editor.pageTitleParsed)
                .then(result => {
                    if (!result.valid) {
                        input.setValidity(false);
                        // optional: show tooltip or mw.notify( result.message )
                    } else {
                        input.setValidity(true);
                    }
                });
        });

        input.on('change', () => {
            let value = input.getValue();
            value = value.replace(new RegExp(`^(https?:)?/{2}?${mw.config.get('wgServer').replace(/^\/{2}/, '')}/wiki/`), '');
            value = value.replace(/^:/, '');
            if (value.length > 0) {
                input.setValue(value[0].toUpperCase() + value.slice(1).replaceAll('_', ' '));
                this.editor.submitButton.setDisabled(false);
                this.editor.showPreviewButton.setDisabled(false);
                this.editor.showChangesButton.setDisabled(false);
            } else {
                this.editor.submitButton.setDisabled(true);
                this.editor.showPreviewButton.setDisabled(true);
                this.editor.showChangesButton.setDisabled(true);
            }
            this.editor.updateSummary();
            this.editor.submitButton.setLabel('保存');
            this.editor.needsCheck = true;
        });

        return new OO.ui.FieldLayout(input, {
            label: '重定向目標:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });
    }

    createTagSelector() {
        const tagOptions = Object.entries(this.editor.redirectTemplates).map(([key, template]) => {
            if (!template.redirect) {
                return { data: key, label: key };
            }
            const label = new OO.ui.HtmlSnippet(`<span class="redirect-helper-redirect-possibilities">${key}</span>`);
            return { data: key, label: label };
        });

        const selector = new OO.ui.MenuTagMultiselectWidget({
            allowArbitrary: false,
            allowReordering: false,
            options: tagOptions
        });

        selector.getMenu().filterMode = 'substring';

        selector.on('change', (items) => {
            const values = items.map(item => item.getData());
            const sortedValues = values.toSorted((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

            if (values.join(';') !== sortedValues.join(';')) {
                const lastItem = items.at(-1);
                selector.reorder(lastItem, sortedValues.indexOf(lastItem.getData()));
            }

            this.editor.updateSummary();
            this.editor.submitButton.setLabel('保存');
            this.editor.needsCheck = true;

            // Hide all template editors
            for (const info of this.editor.templateEditorsInfo) {
                info.details.style.display = 'none';
            }

            // Show only selected template editors
            let count = 0;
            for (const tag of selector.getValue()) {
                const editor = this.editor.templateEditorsInfo.find(info => info.name === tag);
                if (editor) {
                    editor.details.style.display = 'block';
                    count++;
                }
            }

            this.editor.templateParametersEditor.querySelector('summary').textContent =
                `Template parameters (${count > 0 ? `for ${count} template${count > 1 ? 's' : ''}` : 'none to show'})`;
            this.editor.templateParametersEditor.querySelector('#redirect-helper-no-templates-message').style.display =
                count > 0 ? 'none' : 'block';
        });

        return new OO.ui.FieldLayout(selector, {
            label: '重定向分類模板:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });
    }

    createTemplateParametersEditor() {
        return new OO.ui.PanelLayout({
            classes: ['redirect-helper-template-parameters-container']
        });
    }

    createSummaryInput() {
        const input = new OO.ui.ComboBoxInputWidget({
            options: [
                { data: '修復雙重重定向' },
                { data: '修復循環重定向' },
                { data: '移除誤植的重定向分類模板' }
            ]
        });

        return new OO.ui.FieldLayout(input, {
            id: 'redirect-helper-summary-layout',
            label: '編輯摘要:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });
    }

    createActionButtons() {
        const submitButton = new OO.ui.ButtonWidget({
            label: '保存',
            icon: 'check',
            flags: ['primary', 'progressive']
        });

        const previewButton = new OO.ui.ButtonWidget({
            label: '預覽',
            icon: 'eye',
            flags: ['progressive']
        });

        const cancelButton = new OO.ui.ButtonWidget({
            label: '取消',
            icon: 'close',
            flags: ['destructive']
        });

        submitButton.on('click', () => this.editor.handleSubmitButtonClick());
        previewButton.on('click', () => {
            const output = this.editor.createOutput();
            if (output) {
                const dialog = new OutputPreviewDialog({
                    size: 'large'
                }, this.editor.pageTitleParsed);
                dialog.getManager().addWindows([dialog]);
                dialog.getManager().openWindow(dialog);
                dialog.getData = () => output;
            }
        });
        cancelButton.on('click', () => this.handleCancel());

        return new OO.ui.FieldLayout(
            new OO.ui.HorizontalLayout({
                items: [submitButton, previewButton, cancelButton]
            }),
            {
                align: 'left',
                label: '操作'
            }
        );
    }

    createWarningMessage(message, autofixCallback) {
        const warning = new OO.ui.MessageWidget({
            type: 'warning',
            label: message,
            icon: 'alert'
        });

        if (autofixCallback) {
            const autofixButton = new OO.ui.ButtonWidget({
                label: '自動修復',
                flags: ['progressive']
            });

            autofixButton.on('click', () => {
                autofixCallback();
                warning.$element[0].remove();
            });

            warning.$element[0].querySelector('.oo-ui-labelElement-label').append(autofixButton.$element[0]);
        }

        return warning;
    }

    handleCancel() {
        this.editor.editorBox.$element[0].remove();
        if (this.editor.isNewPage) {
            const button = new OO.ui.ButtonWidget({
                id: 'create-redirect-button',
                label: '創建重定向',
                icon: 'articleRedirect',
                flags: ['progressive']
            });

            button.on('click', () => {
                button.$element[0].remove();
                this.editor.load();
            });

            this.editor.contentText.prepend(button.$element[0]);
        } else {
            const portletId = mw.config.get('skin') === 'minerva' ? 'p-tb' : 'p-cactions';
            const portlet = mw.util.addPortletLink(portletId, '#', '編輯重定向', 'redirect-helper');

            portlet.addEventListener('click', (event) => {
                event.preventDefault();
                this.editor.load();
                window.scrollTo({ top: 0, behavior: 'smooth' });
                portlet.remove();
            });
        }
    }

    showValidationDialog(title, message, actions) {
        const dialog = new OO.ui.MessageDialog({
            size: 'medium',
            title,
            message,
            actions
        });

        dialog.getManager().addWindows([dialog]);
        dialog.getManager().openWindow(dialog);

        return dialog;
    }
}

// Editor class
class RedirectEditor {
    constructor(config, exists, createdWatchMethod) {
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        this.redirectTemplates = config.redirectTemplates;
        this.contentText = config.contentText;
        this.pageTitle = config.pageTitle;
        this.pageTitleParsed = config.pageTitleParsed;
        this.exists = exists;
        this.createdWatchMethod = createdWatchMethod;
        this.redirectRegex = /^#.*?:?\s*\[\[\s*:?([^[\]{|}]+?)\s*(?:\|[^[\]{|}]+?)?]]\s*/i;

        // Initialize state
        this.needsCheck = true;
        this.pageContent = '';
        this.oldRedirectTarget = null;
        this.oldRedirectTags = [];
        this.oldRedirectTagData = null;
        this.oldStrayText = '';
        this.parsedDestination = null;

        // Initialize UI components
        this.initializeUIComponents();
    }

    initializeUIComponents() {
        // Initialize redirect input
        this.redirectInput = new RedirectInputWidget({
            placeholder: '目標頁面名稱',
            required: true
        }, this.pageTitleParsed);

        this.redirectInput.on('change', () => {
            let value = this.redirectInput.getValue();
            value = value.replace(new RegExp(`^(https?:)?/{2}?${mw.config.get('wgServer').replace(/^\/{2}/, '')}/wiki/`), '');
            value = value.replace(/^:/, '');
            if (value.length > 0) {
                this.redirectInput.setValue(value[0].toUpperCase() + value.slice(1).replaceAll('_', ' '));
                this.submitButton.setDisabled(false);
                this.showPreviewButton.setDisabled(false);
                this.showChangesButton.setDisabled(false);
            } else {
                this.submitButton.setDisabled(true);
                this.showPreviewButton.setDisabled(true);
                this.showChangesButton.setDisabled(true);
            }
            this.updateSummary();
            this.submitButton.setLabel('保存');
            this.needsCheck = true;
        });

        this.redirectInputLayout = new OO.ui.FieldLayout(this.redirectInput, {
            label: '重定向目標:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });

        // Initialize tag selector
        const tagOptions = Object.entries(this.redirectTemplates).map(([key, template]) => {
            if (!template.redirect) {
                return { data: key, label: key };
            }
            const label = new OO.ui.HtmlSnippet(`<span class="redirect-helper-redirect-possibilities">${key}</span>`);
            return { data: key, label: label };
        });

        this.tagSelect = new OO.ui.MenuTagMultiselectWidget({
            allowArbitrary: false,
            allowReordering: false,
            options: tagOptions
        });

        this.tagSelect.getMenu().filterMode = 'substring';

        this.tagSelect.on('change', (items) => {
            const values = items.map(item => item.getData());
            const sortedValues = values.toSorted((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

            if (values.join(';') !== sortedValues.join(';')) {
                const lastItem = items.at(-1);
                this.tagSelect.reorder(lastItem, sortedValues.indexOf(lastItem.getData()));
            }

            this.updateSummary();
            this.submitButton.setLabel('保存');
            this.needsCheck = true;

            // Hide all template editors
            for (const info of this.templateEditorsInfo) {
                info.details.style.display = 'none';
            }

            // Show only selected template editors
            let count = 0;
            for (const tag of this.tagSelect.getValue()) {
                const editor = this.templateEditorsInfo.find(info => info.name === tag);
                if (editor) {
                    editor.details.style.display = 'block';
                    count++;
                }
            }

            this.templateParametersEditor.querySelector('summary').textContent =
                `Template parameters (${count > 0 ? `for ${count} template${count > 1 ? 's' : ''}` : 'none to show'})`;
            this.templateParametersEditor.querySelector('#redirect-helper-no-templates-message').style.display =
                count > 0 ? 'none' : 'block';
        });

        this.tagSelectLayout = new OO.ui.FieldLayout(this.tagSelect, {
            label: '重定向分類模板:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });

        // Initialize template parameters editor
        this.templateParametersEditor = document.createElement('details');
        this.templateParametersEditor.classList.add('redirect-helper-template-parameters-container');

        const summary = document.createElement('summary');
        summary.textContent = '模板參數(無)';
        this.templateParametersEditor.append(summary);

        this.templateEditorsInfo = [];

        for (const [tag, template] of Object.entries(this.redirectTemplates)) {
            const parameters = Object.entries(template.parameters);
            if (parameters.length === 0) continue;

            const details = document.createElement('details');
            details.style.display = 'none';

            const paramSummary = document.createElement('summary');
            paramSummary.textContent = tag;
            details.append(paramSummary);

            const editorInfo = {
                name: tag,
                details: details,
                parameters: []
            };

            for (const [param, config] of parameters) {
                const input = new OO.ui.TextInputWidget({
                    placeholder: config.default?.toString(),
                    required: config.required
                });

                input.on('change', () => {
                    this.updateSummary();
                    this.submitButton.setLabel('保存');
                    this.needsCheck = true;
                });

                const layout = new OO.ui.FieldLayout(input, {
                    label: new OO.ui.HtmlSnippet(
                        `${param}${!config.label || param.toLowerCase() === config.label.toLowerCase() ? '' : ` (${config.label})`}` +
                        `${config.description ? ` (${config.description})` : ''}` +
                        ` (type: ${config.type})` +
                        `${config.suggested ? ' (建議)' : ''}` +
                        `${config.example ? ` (例:"${config.example}")` : ''}`
                    ),
                    align: 'inline'
                });

                details.append(layout.$element[0]);
                editorInfo.parameters.push({
                    name: param,
                    aliases: config.aliases,
                    editor: input
                });
            }

            this.templateParametersEditor.append(details);
            this.templateEditorsInfo.push(editorInfo);
        }

        const noTemplatesMessage = document.createElement('div');
        noTemplatesMessage.id = 'redirect-helper-no-templates-message';
        noTemplatesMessage.textContent = '無模板參數';
        this.templateParametersEditor.append(noTemplatesMessage);

        // Initialize summary input
        this.summaryInput = new OO.ui.ComboBoxInputWidget({
            options: [
                { data: '修復雙重重定向' },
                { data: '修復循環重定向' },
                { data: '移除誤植的重定向分類模板' }
            ]
        });

        this.summaryInputLayout = new OO.ui.FieldLayout(this.summaryInput, {
            id: 'redirect-helper-summary-layout',
            label: '編輯摘要:',
            classes: ['redirect-input-layout'],
            align: 'top'
        });
    }

    async load() {
        this.editorBox = new OO.ui.PanelLayout({
            id: 'redirect-helper-box',
            padded: true,
            expanded: false,
            framed: true
        });

        this.loadInputElements();
        await this.loadSubmitElements();

        // Add the submit layout to the editor box
        this.editorBox.$element.append(this.submitLayout.$element);

        this.contentText.prepend(this.editorBox.$element[0]);

        if (this.exists) {
            await this.loadExistingData();
        }
    }

    loadInputElements() {
        // Create a container for all input elements
        const inputContainer = $('<div>').addClass('input-container');

        // Add all input elements to the container
        inputContainer.append(
            this.redirectInputLayout.$element,
            this.tagSelectLayout.$element,
            this.templateParametersEditor.$element,
            this.summaryInputLayout.$element
        );

        // Add the input container to the editor box
        this.editorBox.$element.append(inputContainer);
    }

    async loadSubmitElements() {
        const windowManager = new OO.ui.WindowManager();
        document.body.appendChild(windowManager.$element[0]);

        this.submitButton = new OO.ui.ButtonWidget({
            label: '保存',
            disabled: true,
            flags: ['progressive']
        });
        this.submitButton.on('click', () => this.handleSubmitButtonClick());

        // Initialize OutputPreviewDialog
        const outputPreviewDialog = new OutputPreviewDialog({
            size: 'large'
        }, this.pageTitleParsed);
        windowManager.addWindows([outputPreviewDialog]);

        this.showPreviewButton = new OO.ui.ButtonWidget({
            label: '顯示預覽',
            disabled: true
        });
        this.showPreviewButton.on('click', () => {
            outputPreviewDialog.setData(this.createOutput(
                this.redirectInput.getValue(),
                this.tagSelect.getValue(),
                this.oldStrayText,
            ));
            outputPreviewDialog.open();
        });

        // Initialize ChangesDialog
        const changesDialog = new ChangesDialog({
            size: 'large'
        });
        windowManager.addWindows([changesDialog]);

        this.showChangesButton = new OO.ui.ButtonWidget({
            label: '顯示變更',
            disabled: true
        });
        this.showChangesButton.on('click', async () => {
            if (this.exists) {
                this.pageContent = await this.getPageContent(this.pageTitle);
            }
            changesDialog.setData([
                this.pageContent,
                this.createOutput(
                    this.redirectInput.getValue(),
                    this.tagSelect.getValue(),
                    this.oldStrayText,
                )
            ]);
            changesDialog.open();
        });

        // Handle talk page sync checkbox
        if (!this.pageTitleParsed.isTalkPage()) {
            this.talkData = await this.api.get({
                action: 'query',
                formatversion: '2',
                prop: 'info',
                titles: this.pageTitleParsed.getTalkPage().getPrefixedText()
            });
            this.syncTalkCheckbox = new OO.ui.CheckboxInputWidget({
                selected: !!this.talkData.query.pages[0].redirect
            });
            this.syncTalkCheckboxLayout = new OO.ui.Widget({
                content: [
                    new OO.ui.FieldLayout(this.syncTalkCheckbox, {
                        label: '同步討論頁',
                        align: 'inline'
                    })
                ]
            });
        }

        // Handle watch checkbox for new pages
        if (!this.exists) {
            const watchConfig = {};
            if (['nochange', 'preferences'].includes(this.createdWatchMethod)) {
                watchConfig.indeterminate = true;
            } else {
                watchConfig.selected = this.createdWatchMethod === 'watch';
            }
            this.watchCheckbox = new OO.ui.CheckboxInputWidget(watchConfig);
            this.watchCheckboxLayout = new OO.ui.Widget({
                content: [
                    new OO.ui.FieldLayout(this.watchCheckbox, {
                        label: '監視頁面',
                        align: 'inline'
                    })
                ]
            });
        }

        // Handle patrol checkbox
        if (await this.checkShouldPromptPatrol()) {
            this.patrolCheckbox = new OO.ui.CheckboxInputWidget({
                selected: true
            });
            this.patrolCheckboxLayout = new OO.ui.Widget({
                content: [
                    new OO.ui.FieldLayout(this.patrolCheckbox, {
                        label: '標記為已巡查',
                        align: 'inline'
                    })
                ]
            });
        }

        this.submitLayout = new OO.ui.HorizontalLayout({
            id: 'redirect-helper-submit-layout',
            items: [
                this.submitButton,
                this.showPreviewButton,
                this.showChangesButton,
                this.syncTalkCheckboxLayout,
                this.watchCheckboxLayout,
                this.patrolCheckboxLayout
            ].filter(Boolean)
        });
    }

    async checkShouldPromptPatrol() {
        console.log("Starting checkShouldPromptPatrol");
        console.log("Current namespace:", mw.config.get("wgNamespaceNumber"));
        console.log("Article ID:", mw.config.get("wgArticleId"));

        const patrolIcon = document.querySelector("#mwe-pt-mark .mwe-pt-tool-icon");
        console.log("Patrol icon found:", !!patrolIcon);
        if (patrolIcon) {
            console.log("Attempting to click patrol icon");
            patrolIcon.click();
            patrolIcon.click();
        }

        if (mw.config.get("wgNamespaceNumber") !== 0) {
            console.log("Not in main namespace, returning false");
            return false;
        }

        if (document.querySelector(".patrollink")) {
            console.log("Found patrollink, returning true");
            return true;
        }

        if (document.querySelector("#mwe-pt-mark-as-reviewed-button")) {
            console.log("Found mark-as-reviewed button, returning true");
            return true;
        }

        if (document.querySelector("#mwe-pt-mark-as-unreviewed-button")) {
            console.log("Found mark-as-unreviewed button, returning false");
            return false;
        }

        try {
            const userRights = await mw.user.getRights();
            console.log("User rights:", userRights);

            if (!mw.config.get("wgArticleId") || !userRights.includes("patrol")) {
                console.log("No article ID or no patrol rights, returning false");
                return false;
            }

            console.log("Fetching recent changes for article ID:", mw.config.get("wgArticleId"));
            const result = await this.api.get({
                action: "query",
                list: "recentchanges",
                rctitle: mw.config.get("wgPageName"),
                rcprop: "patrolled|user|autopatrolled",
                rclimit: 1
            }).catch(error => {
                console.log("API error details:", error);
                return null;
            });

            console.log("Recent changes result:", result);

            if (!result || !result.query?.recentchanges?.length) {
                console.log("No recent changes found");
                return false;
            }

            const change = result.query.recentchanges[0];
            console.log("Most recent change:", change);

            // Check if the change needs patrolling
            // Empty string for patrolled means it needs patrolling
            // Empty string for autopatrolled means it's not autopatrolled
            const needsPatrolling = change.patrolled === "" && change.autopatrolled === "" && change.user !== mw.config.get("wgUserName");
            console.log("Needs patrolling:", needsPatrolling);
            return needsPatrolling;
        } catch (error) {
            console.error("Error in checkShouldPromptPatrol:", error);
            return false;
        }
    }

    updateSummary() {
        const redirectTarget = this.redirectInput.getValue().trim();
        if (!redirectTarget) {
            this.summaryInput.$tabIndexed[0].placeholder = '';
            return;
        }

        if (this.exists) {
            let oldTarget = this.oldRedirectTarget?.replaceAll('_', ' ');
            if (oldTarget) {
                oldTarget = oldTarget[0].toUpperCase() + oldTarget.slice(1);
            }

            const targetChanged = redirectTarget !== oldTarget;
            const tagsChanged = this.tagSelect.getValue().some(tag => !this.oldRedirectTags.includes(tag)) ||
                this.oldRedirectTags.some(tag => !this.tagSelect.getValue().includes(tag));

            let templateParamsChanged = false;
            if (this.oldRedirectTagData) {
                const templatesWithParams = Object.entries(this.redirectTemplates)
                    .filter(([, template]) => Object.entries(template.parameters).length > 0);

                for (const [tag, template] of templatesWithParams) {
                    if (!this.oldRedirectTags.includes(tag) || !this.tagSelect.getValue().includes(tag)) {
                        continue;
                    }

                    const oldParams = this.oldRedirectTagData[tag] ??
                        Object.entries(template.parameters).map(([name]) => [name, '']);
                    const editor = this.templateEditorsInfo.find(info => info.name === tag);

                    for (const param of editor.parameters) {
                        const oldValue = oldParams.find(([name]) => name === param.name)?.[1] ?? '';
                        const newValue = param.editor.getValue().trim();

                        if (oldValue !== newValue) {
                            templateParamsChanged = true;
                            break;
                        }
                    }

                    if (templateParamsChanged) break;
                }
            }

            const changes = [];
            if (targetChanged) changes.push(`修改重定向目標到「[[${redirectTarget}]]」`);
            if (tagsChanged) {
                changes.push(
                    this.tagSelect.getValue().length > 0 && this.oldRedirectTags.length > 0 ? '修改' :
                        this.tagSelect.getValue().length > 0 ? '添加' : '移除',
                    '分類模板'
                );
            }
            if (templateParamsChanged) changes.push('修改分類模板參數');

            if (changes.length === 0) {
                changes.push('清理重定向');
            }

            changes[0] = changes[0][0].toUpperCase() + changes[0].slice(1);
            if (changes.length > 1) {
                changes[changes.length - 1] = `${changes.at(-1)}`;
            }

            this.summaryInput.$tabIndexed[0].placeholder = changes.join(changes.length > 2 ? '、' : '');
        } else {
            this.summaryInput.$tabIndexed[0].placeholder = `新重定向到 [[${redirectTarget}]]`;
        }
    }

    async handleSubmitButtonClick() {
        const elements = [
            this.redirectInput,
            this.tagSelect,
            ...this.templateEditorsInfo.flatMap(info => info.parameters.map(param => param.editor)),
            this.summaryInput,
            this.submitButton,
            this.showPreviewButton,
            this.showChangesButton,
            this.syncTalkCheckbox,
            this.watchCheckbox,
            this.patrolCheckbox
        ].filter(Boolean);

        for (const element of elements) {
            element.setDisabled(true);
        }

        this.submitButton.setLabel('檢查目標頁面是否存在……');

        let errors = [];
        if (this.needsCheck) {
            errors = await this.validateSubmission();
        } else {
            this.parsedDestination = mw.Title.newFromText(this.redirectInput.getValue());
        }

        if (errors.length > 0) {
            for (const warning of document.querySelectorAll('.redirect-helper-warning')) {
                warning.remove();
            }

            for (const { title, message, autoFixes } of errors) {
                const label = new OO.ui.HtmlSnippet(
                    `${title ? `<a href="${mw.util.getUrl(title)}" target="_blank">${title}</a>` : '本頁面'} ${message} 再次點擊保存可無視錯誤提交。`
                );

                const warning = new OO.ui.MessageWidget({
                    type: 'error',
                    classes: ['redirect-helper-warning'],
                    inline: true,
                    label
                });

                if (autoFixes) {
                    const autofixButton = new OO.ui.ButtonWidget({
                        label: '自動修復',
                        flags: ['progressive'],
                        classes: ['redirect-helper-autofix-button']
                    });

                    autofixButton.on('click', () => {
                        const currentTags = this.tagSelect.getValue();
                        for (const fix of autoFixes) {
                            if (fix.type === 'add' && !currentTags.includes(fix.tag)) {
                                this.tagSelect.addTag(fix.tag, fix.tag);
                            } else if (fix.type === 'remove' && currentTags.includes(fix.tag)) {
                                this.tagSelect.removeTagByData(fix.tag);
                            } else if (fix.type === 'change-target') {
                                this.redirectInput.setValue(fix.target);
                            }
                        }
                        warning.$element[0].style.textDecoration = 'line-through 2px black';
                        autofixButton.$element[0].remove();
                    });

                    warning.$element[0].querySelector('.oo-ui-labelElement-label').append(autofixButton.$element[0]);
                }

                this.editorBox.$element[0].append(warning.$element[0]);
            }

            for (const element of elements) {
                element.setDisabled(false);
            }

            this.submitButton.setLabel('仍保存');
            this.needsCheck = false;
            return;
        }

        this.submitButton.setLabel(`正在${this.exists ? '修改' : '創建'}重定向……`);

        const output = this.createOutput(
            this.redirectInput.getValue(),
            this.tagSelect.getValue(),
            this.oldStrayText,
        );

        const summary = (this.summaryInput.getValue() || this.summaryInput.$tabIndexed[0].placeholder) + CONFIG.SCRIPT_MESSAGE;

        if (await this.editOrCreate(this.pageTitle, output, summary)) {
            mw.notify(`重定向${this.exists ? '修改' : '創建'}成功!`, { type: 'success' });

            if (this.syncTalkCheckbox?.isSelected()) {
                this.submitButton.setLabel('正在編輯討論頁...');
                const isMove = this.tagSelect.getValue().includes('移動重定向');
                const talkOutput = this.createOutput(
                    this.parsedDestination.getTalkPage().getPrefixedText(),
                    isMove ? ['移動重定向'] : [],
                    undefined,
                    undefined,
                    []
                );

                if (!await this.editOrCreate(
                    this.pageTitleParsed.getTalkPage().getPrefixedText(),
                    talkOutput,
                    '同步主頁面重定向' + CONFIG.SCRIPT_MESSAGE
                )) {
                    return;
                }

                mw.notify('討論頁同步成功!', { type: 'success' });
            }

            if (this.patrolCheckbox?.isSelected()) {
                this.submitButton.setLabel('正在巡查重定向……');
                const patrolLink = document.querySelector('.patrollink a');
                const reviewButton = document.querySelector('#mwe-pt-mark-as-reviewed-button');

                if (patrolLink) {
                    if (await this.api.postWithToken('patrol', {
                        action: 'patrol',
                        rcid: new URL(patrolLink.href).searchParams.get('rcid')
                    }).catch((error, result) => {
                        mw.notify(`巡查「${this.pageTitle}」失敗:${result?.error?.info ?? '未知錯誤'} (${error})`, { type: 'error' });
                        return null;
                    })) {
                        mw.notify('重定向巡查成功!', { type: 'success' });
                    }
                } else if (reviewButton) {
                    reviewButton.click();
                    mw.notify('重定向巡查成功!', { type: 'success' });
                } else {
                    mw.notify('未找到頁面巡查工具欄,巡查失敗!', { type: 'error' });
                }
            }

            this.submitButton.setLabel('完成,正在重新載入頁面……');
            window.location.href = mw.util.getUrl(this.pageTitle, { redirect: 'no' });
        }
    }

    createOutput(target, tags, strayText) {
        const parsedTarget = mw.Title.newFromText(target);
        const targetText = parsedTarget ?
            `${parsedTarget.getNamespaceId() === 14 ? ':' : ''}${parsedTarget.getPrefixedText()}${parsedTarget.getFragment() ? `#${parsedTarget.getFragment()}` : ''}` :
            target.trim();

        const templateOutput = tags.map(tag => {
            const editor = this.templateEditorsInfo.find(info => info.name === tag);
            if (!editor) return `{{${tag}}}`;

            const lastPositionalIndex = editor.parameters.findLastIndex((param, index) =>
                param.name === (index + 1).toString() && param.editor.getValue().trim()
            );

            const params = editor.parameters.map((param, index) => {
                const value = param.editor.getValue().trim();
                if (!value && index > lastPositionalIndex) return null;
                return `|${param.name === (index + 1).toString() ? '' : `${param.name}=`}${value}`;
            }).filter(Boolean).join('');

            return `{{${tag}${params}}}`;
        });

        return [
            `#REDIRECT [[${targetText}]]`,
            tags.length > 0 ? `{{Redirect category shell|\n${templateOutput.join('\n')}\n}}` : null,
            strayText ? strayText + '\n' : null,
        ].filter(Boolean).join('\n');
    }

    async getPageContent(title) {
        return (await this.api.get({
            action: 'query',
            formatversion: '2',
            prop: 'revisions',
            rvprop: 'content',
            rvslots: 'main',
            titles: title
        })).query.pages[0].revisions[0].slots.main.content.trim();
    }

    async editOrCreate(title, content, summary) {
        let watchlist = 'preferences';
        if (this.watchCheckbox) {
            if (this.watchCheckbox.isIndeterminate()) {
                watchlist = this.createdWatchMethod;
            } else if (this.watchCheckbox.isSelected()) {
                watchlist = 'watch';
            } else {
                watchlist = 'unwatch';
            }
        }

        return await this.api.edit(title, () => ({ text: content, summary }))
            .catch((error, result) => {
                if (error === 'nocreate-missing') {
                    return this.api.create(title, { summary, watchlist }, content)
                        .catch((error, result) => {
                            mw.notify(`創建「${title}」失敗:${result?.error?.info ?? '未知錯誤'} (${error})`, { type: 'error' });
                            return null;
                        });
                }
                mw.notify(`創建或編輯「${title}」失敗:${result?.error?.info ?? '未知錯誤'} (${error})`, { type: 'error' });
                return null;
            });
    }

    async validateSubmission() {
        const errors = [];
        const redirectTarget = this.redirectInput.getValue().trim();
        const selectedTags = this.tagSelect.getValue();

        // Basic title validation
        if (!/^\s*[^[\]{|}]+\s*$/.test(redirectTarget)) {
            errors.push({
                title: redirectTarget,
                message: '並非有效的頁面名稱!'
            });
        }

        // Parse target title
        try {
            this.parsedDestination = mw.Title.newFromText(redirectTarget);
        } catch {
            if (errors.length === 0) {
                errors.push({
                    title: redirectTarget,
                    message: '並非有效的頁面名稱!'
                });
            }
        }

        if (!this.parsedDestination && errors.length === 0) {
            errors.push({
                title: redirectTarget,
                message: '並非有效的頁面名稱!'
            });
        }

        // Check self-redirect
        if (this.parsedDestination?.getPrefixedText() === this.pageTitleParsed.getPrefixedText()) {
            errors.push({
                message: '不能重定向到自身!',
            });
        }

        // Check target existence and properties
        const targetInfo = await this.api.get({
            action: 'query',
            formatversion: '2',
            prop: ['pageprops', 'categories'],
            titles: redirectTarget
        }).catch(error => {
            if (error === 'missingtitle') {
                errors.push({
                    title: redirectTarget,
                    message: '重定向目標不存在!'
                });
            } else {
                errors.push({
                    title: redirectTarget,
                    message: `無法通過 API 獲取 (${error})!`
                });
            }
            return null;
        });

        // Check for redirects
        let parseInfo;
        try {
            parseInfo = await this.api.get({
                action: 'parse',
                page: redirectTarget,
                prop: 'sections',
                redirects: true
            });
        } catch (error) {
            if (error === 'missingtitle') {
                errors.push({
                    title: redirectTarget,
                    message: '重定向目標不存在!'
                });
            } else {
                errors.push({
                    title: redirectTarget,
                    message: `parse API 錯誤 (${error})!`
                });
            }
            // give yourself a safe default so the rest of the logic can run
            parseInfo = { parse: { redirects: [], sections: [] } };
        }

        if (parseInfo.parse.redirects.length > 0) {
            const redirectTarget = parseInfo.parse.redirects[0].to +
                (parseInfo.parse.redirects[0].tofragment ? `#${parseInfo.parse.redirects[0].tofragment}` : '');
            errors.push({
                title: redirectTarget,
                message: `已重定向到「<a href="${mw.util.getUrl(redirectTarget)}" target="_blank">${redirectTarget}</a>」。請直接重定向到該頁面,以避免雙重重定向!`,
                autoFixes: [{
                    type: 'change-target',
                    target: redirectTarget
                }]
            });
        }

        // Check section/anchor redirects
        if (redirectTarget.split('#').length > 1) {
            if (parseInfo.parse.sections.find(section =>
                section.line.replaceAll(/<\/?i>/g, '') === redirectTarget.split('#')[1])) {
                if (selectedTags.includes('章節重定向')) {
                    errors.push({
                        message: '被標記為「章節重定向」,但其實是錨點重定向!',
                        autoFixes: [
                            { type: 'add', tag: '錨點重定向' },
                            { type: 'remove', tag: '章節重定向' }
                        ]
                    });
                } else if (!selectedTags.includes('章節重定向')) {
                    errors.push({
                        message: '是錨點重定向,但尚未被標記上 <code>{{錨點重定向}}</code>!',
                        autoFixes: [{ type: 'add', tag: '錨點重定向' }]
                    });
                }
            } else {
                const targetContent = (await this.api.get({
                    action: 'query',
                    formatversion: '2',
                    prop: 'revisions',
                    rvprop: 'content',
                    rvslots: 'main',
                    titles: this.parsedDestination.getPrefixedText()
                })).query.pages[0].revisions[0].slots.main.content;

                const anchors = [
                    ...targetContent.match(/(?<={{\s*?[Aa](?:nchors?|nchor for redirect|nker|NCHOR|nc)\s*?\|).+?(?=}})/g)?.map(a =>
                        a.split('|').map(p => p.trim())).flat() ?? [],
                    ...targetContent.match(/(?<={{\s*?(?:[Vv](?:isible anchors?|isanc|Anch|anchor|isibleanchor|a)|[Aa](?:nchord|chored|nchor\+)|[Tt]ext anchor)\s*?\|).+?(?=(?<!!|=)}})/g)?.map(a =>
                        a.split('|').map(p => p.trim()).filter(p => !/^text\s*?=/.test(p))).flat() ?? [],
                    ...targetContent.match(/(?<=id=)"?.+?(?="|>|\|)/g)?.map(a => a.trim()) ?? [],
                    ...targetContent.match(/EpisodeNumber += +\d+/g)?.map(a => `ep${a.split('=')[1].trim()}`) ?? []
                ];

                if (anchors.includes(redirectTarget.split('#')[1])) {
                    if (selectedTags.includes('章節重定向')) {
                        errors.push({
                            message: '被標記為「章節重定向」,但其實是錨點重定向!',
                            autoFixes: [
                                { type: 'add', tag: '錨點重定向' },
                                { type: 'remove', tag: '章節重定向' }
                            ]
                        });
                    } else if (!selectedTags.includes('章節重定向')) {
                        errors.push({
                            message: '是錨點重定向,但尚未被標記上 <code>{{錨點重定向}}</code>!',
                            autoFixes: [{ type: 'add', tag: '錨點重定向' }]
                        });
                    }
                } else {
                    errors.push({
                        message: `重定向到「<a href="${mw.util.getUrl(redirectTarget)}」" target="_blank">${redirectTarget}</a>,但該章節或錨點並不存在!`,
                        autoFixes: [{
                            type: 'change-target',
                            target: redirectTarget.split('#')[0]
                        }]
                    });
                }
            }
        }

        // Check for section/anchor tags on non-section/anchor redirects
        if (redirectTarget.split('#').length === 1) {
            for (const tag of ['章節重定向', '錨點重定向']) {
                if (selectedTags.includes(tag)) {
                    errors.push({
                        message: `並非到章節或錨點的重定向,但卻被誤植了 <code>{{${tag}}}</code>!`,
                        autoFixes: [{ type: 'remove', tag }]
                    });
                }
            }
        }

        // Check disambiguation page handling
        const isDisambiguation = !!(targetInfo.query.pages[0].pageprops && 'disambiguation' in targetInfo.query.pages[0].pageprops);
        const isSurnameList = !!targetInfo.query.pages[0].categories?.some(cat => cat.title === 'Category:姓氏');
        const disambiguationTags = ['消歧義頁重定向', 'R from incomplete disambiguation'];
        const ambiguousTags = ['R from ambiguous sort name', 'R from ambiguous term'];
        const hasDisambiguationTag = disambiguationTags.some(tag => selectedTags.includes(tag));
        const hasAmbiguousTag = ambiguousTags.some(tag => selectedTags.includes(tag));

        if (isDisambiguation && !hasDisambiguationTag && !hasAmbiguousTag) {
            errors.push({
                message: '是到消歧義頁的重定向,但並未被標記上 <code>{{消歧義頁重定向}}</code>!'
            });
        }

        if (targetInfo.query.pages[0].pageprops && !isDisambiguation) {
            if ((!isSurnameList && (hasDisambiguationTag || hasAmbiguousTag)) || (isSurnameList && hasDisambiguationTag)) {
                errors.push({
                    message: '並非到消歧義頁的重定向,但卻被標記了 <code>{{消歧義頁重定向}}</code>!',
                    autoFixes: [...disambiguationTags, ...ambiguousTags].map(tag => ({ type: 'remove', tag }))
                });
            }
            if (isSurnameList && !hasAmbiguousTag) {
                errors.push({
                    message: '是到同姓列表的重定向,但並未被標記上正確的消歧義分類模板!'
                });
            }
        }

        // if (isDisambiguation && selectedTags.includes('消歧義頁重定向') &&
        //     !this.pageTitleParsed.getMainText().endsWith(' (disambiguation)')) {
        //     errors.push({
        //         message: '被標記了 <code>{{消歧義頁重定向}}</code>,但頁面名稱並未以 " (消歧義)" 結尾。Use <code>{{R from ambiguous term}}</code> or a similar categorization template instead!',
        //         autoFixes: [{ type: 'remove', tag: '消歧義頁重定向' }]
        //     });
        // }

        // Check protection tags
        for (const tag of ['R protected', 'R semi-protected', 'R extended-protected', 'R template-protected', 'R fully protected']) {
            if (selectedTags.includes(tag)) {
                errors.push({
                    message: `is tagged with unnecessarily tagged with <code>{{${tag}}}</code> which will be duplicated by the redirect category shell!`,
                    autoFixes: [{ type: 'remove', tag }]
                });
            }
        }

        // Check Wikidata item
        if (mw.config.get('wgWikibaseItemId') && !selectedTags.includes('維基數據項目重定向')) {
            errors.push({
                message: '被連接到維基數據項目,但尚未被標記上 <code>{{維基數據項目重定向}}</code>!',
                autoFixes: [{ type: 'add', tag: '維基數據項目重定向' }]
            });
        } else if (selectedTags.includes('維基數據項目重定向') && !mw.config.get('wgWikibaseItemId')) {
            errors.push({
                message: '被標記了 <code>{{維基數據項目重定向}}</code> 但還未連接到維基數據項目!',
                autoFixes: [{ type: 'remove', tag: '維基數據項目重定向' }]
            });
        }

        // Check template parameters
        for (const tag of selectedTags) {
            if (!(tag in this.redirectTemplates)) continue;
            const template = this.redirectTemplates[tag];
            for (const [param, config] of Object.entries(template.parameters)) {
                const editor = this.templateEditorsInfo.find(info => info.name === tag)
                    ?.parameters.find(p => [p.name, ...p.aliases].includes(param));
                if (editor && config.required && !editor.editor.getValue().trim()) {
                    errors.push({
                        message: `被標記了 <code>{{${tag}}}</code> 但缺少必要參數「<code>${param}</code>」!`
                    });
                }
            }
        }

        // Check talk page sync
        if (this.syncTalkCheckbox?.isSelected() &&
            !this.talkData.query.pages[0].missing &&
            !this.talkData.query.pages[0].redirect) {
            errors.push({
                title: this.pageTitleParsed.getTalkPage().getPrefixedText(),
                message: '已存在,但並非重定向!'
            });
        }

        return errors;
    }

    async loadExistingData() {
        if (this.exists) {
            this.pageContent = await this.getPageContent(this.pageTitle);
        }
        this.oldRedirectTarget = this.redirectRegex.exec(this.pageContent)?.[1];
        this.oldRedirectTags = Object.entries(this.redirectTemplates)
            .map(([name, template]) =>
                [name, ...template.aliases].some(alias =>
                    new RegExp(`{{\\s*[${alias[0].toLowerCase()}${alias[0]}]${alias.slice(1)}\\s*(\\||}})`)
                        .test(this.pageContent)
                ) ? name : null
            )
            .filter(Boolean)
            .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

        // Fix the template parameter extraction regex
        const templateMatches = Object.entries(this.redirectTemplates)
            .flatMap(([name, template]) =>
                [name, ...template.aliases].map(alias => {
                    const regex = new RegExp(`{{\\s*[${alias[0].toLowerCase()}${alias[0]}]${alias.slice(1)}\\s*\\|?([^}]*?)\\s*}}`);
                    const match = regex.exec(this.pageContent);
                    return match ? [name, match[1]] : null;
                })
            )
            .filter(Boolean);

        this.oldRedirectTagData = Object.fromEntries(
            templateMatches.map(([name, params]) => {
                if (!params) return null;
                const paramList = params.split('|')
                    .map((param, index) => {
                        if (!param.includes('=')) {
                            return [(index + 1).toString(), param.trim()];
                        }
                        const [key, value] = param.split('=');
                        return [key.trim(), value.trim()];
                    });
                return [name, paramList];
            })
                .filter(Boolean)
        );

        this.oldStrayText = [
            /{{short description\|.*?}}/i.exec(this.pageContent)?.[0],
            /{{DISPLAYTITLE:.*?}}/.exec(this.pageContent)?.[0],
            /{{italic title\|?.*?}}/i.exec(this.pageContent)?.[0],
            /{{title language\|.*?}}/.exec(this.pageContent)?.[0],
            /{{authority control(\|.*?)?}}/i.exec(this.pageContent)?.[0]
        ].filter(Boolean).join('\n');

        if (this.oldRedirectTarget) {
            this.redirectInput.setValue(this.oldRedirectTarget.replaceAll('_', ' '));
        } else {
            mw.notify('[RedirectHelper] 找不到重定向目標!', { type: 'error' });
        }

        this.tagSelect.setValue(this.oldRedirectTags);

        for (const [name, params] of Object.entries(this.oldRedirectTagData)) {
            const templateInfo = this.templateEditorsInfo.find(info => info.name === name);
            if (templateInfo) {
                for (const [paramName, value] of params) {
                    const param = templateInfo.parameters.find(p => [p.name, ...p.aliases].includes(paramName));
                    if (param) {
                        param.editor.setValue(value);
                    }
                }
            }
        }

        this.updateSummary();
    }
}

// Main RedirectHelper class
class RedirectHelper {
    constructor() {
        this.api = new mw.Api({ userAgent: 'RedirectHelper/2.0.0' });
        this.redirectTemplates = null;
        this.contentText = null;
        this.pageTitle = null;
        this.pageTitleParsed = null;
        this.createdWatchMethod = null;
    }

    async run() {
        if (!this.passesPreChecks()) return;

        this.redirectTemplates = await Utils.fetchRedirectTemplates(this.api);
        this.contentText = document.querySelector('#mw-content-text');

        if (!this.contentText) {
            mw.notify('[RedirectHelper] 找不到 #mw-content-text 元素!', { type: 'error' });
            return;
        }

        this.pageTitle = mw.config.get('wgPageName');
        this.pageTitleParsed = mw.Title.newFromText(this.pageTitle);

        if (!this.pageTitleParsed) {
            mw.notify('[RedirectHelper] 無法讀取頁面標題!', { type: 'error' });
            return;
        }

        const configWatchMethod = window.redirectHelperConfiguration?.createdWatchMethod;
        this.createdWatchMethod = configWatchMethod && ['nochange', 'preferences', 'unwatch', 'watch'].includes(configWatchMethod)
            ? configWatchMethod
            : 'preferences';

        await this.checkPageAndLoad();
    }

    passesPreChecks() {
        return [
            mw.config.get('wgNamespaceNumber') >= 0,
            mw.config.get('wgIsProbablyEditable'),
            ['view', 'edit'].includes(mw.config.get('wgAction')),
            (mw.config.get('wgRevisionId') || mw.config.get('wgCurRevisionId')) === mw.config.get('wgCurRevisionId'),
            !mw.config.get('wgDiffOldId')
        ].every(Boolean);
    }

    async checkPageAndLoad() {
        mw.util.addCSS(CONFIG.STYLES);

        const pageInfo = await this.api.get({
            action: 'query',
            formatversion: '2',
            prop: 'info',
            titles: this.pageTitle
        });

        const config = {
            redirectTemplates: this.redirectTemplates,
            contentText: this.contentText,
            pageTitle: this.pageTitle,
            pageTitleParsed: this.pageTitleParsed
        };

        if (pageInfo.query.pages[0].missing) {
            this.setupCreateRedirectButton(config);
        } else if (pageInfo.query.pages[0].redirect) {
            new RedirectEditor(config, true, this.createdWatchMethod).load();
        } else {
            this.setupRedirectPortlet(config);
        }
    }

    setupCreateRedirectButton(config) {
        const button = new OO.ui.ButtonWidget({
            id: 'create-redirect-button',
            label: '創建重定向',
            icon: 'articleRedirect',
            flags: ['progressive']
        });

        button.on('click', () => {
            button.$element[0].remove();
            new RedirectEditor(config, false, this.createdWatchMethod).load();
        });

        this.contentText.prepend(button.$element[0]);
    }

    setupRedirectPortlet(config) {
        const portletId = mw.config.get('skin') === 'minerva' ? 'p-tb' : 'p-cactions';
        const portlet = mw.util.addPortletLink(portletId, '#', '編輯重定向', 'redirect-helper');

        portlet.addEventListener('click', (event) => {
            event.preventDefault();
            new RedirectEditor(config, false, this.createdWatchMethod).load();
            window.scrollTo({ top: 0, behavior: 'smooth' });
            portlet.remove();
        });
    }
}

// Initialize the helper when required modules are loaded
mw.loader.using(CONFIG.REQUIRED_MODULES, () => {
    new RedirectHelper().run();
});
// </nowiki>