跳转到内容

User:SuperGrey/gadgets/CiteUnseen/main.js

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

(function () {

    const CiteUnseen = {
        /**
         * 解析 COinS 字串,並將其轉換為 object。
         * @param query {String} - COinS 字串
         * @returns {Object} - 解析後的 object
         */
        parseCoinsString: function (query) {
            const result = {};
            // Split the string by '&'
            const pairs = query.split('&');
            pairs.forEach(pair => {
                if (!pair) return;
                // Split at the first '='
                const index = pair.indexOf('=');
                let key, value;
                if (index === -1) {
                    key = pair;
                    value = "";
                } else {
                    key = pair.substring(0, index);
                    value = pair.substring(index + 1);
                }
                // Replace '+' with space and decode both key and value
                key = key.replace(/\+/g, ' ');
                value = value.replace(/\+/g, ' ');
                if (!result[key]) {
                    result[key] = value;
                } else {
                    if (typeof result[key] === 'string') {
                        result[key] = [result[key]];
                    }
                    result[key].push(value);
                }
            });
            return result;
        },

        /**
         * 解析包含逗號分隔條件的日期規則字串,
         * 每個條件包含一個運算符和一個日期。
         * 返回一個接受日期(或日期字串)的判斷函式,
         * 若日期符合所有條件則返回 true。
         * @param ruleString {String} - 日期規則字串,例如 "<2022-01-01,>2020-01-01"
         * @returns {Function|null} - 判斷函式,若有任何條件無效則返回 null。
         */
        parseDateRule: function (ruleString) {
            ruleString = ruleString.trim();
            if (ruleString.length === 0) {
                return null;
            }

            // Split the rule string by comma into individual conditions.
            let conditionStrings = ruleString.split(',').map(s => s.trim());

            // Array to hold predicate functions for each condition.
            let predicates = [];

            for (let cond of conditionStrings) {
                if (cond.length === 0) continue; // Skip empty conditions

                // Check if the condition starts with an operator (<, >, or =). Default to "=".
                let operator = '=';
                if (cond[0] === '<' || cond[0] === '>' || cond[0] === '=') {
                    operator = cond[0];
                    cond = cond.substring(1).trim();
                }

                // Parse the date portion.
                let targetDate = new Date(cond);
                if (isNaN(targetDate.getTime())) {
                    // If any condition has an invalid date, return null.
                    return null;
                }

                // Create and store a predicate function for this condition.
                let predicate;
                switch (operator) {
                    case '<':
                        predicate = function (date) {
                            return date < targetDate;
                        };
                        break;
                    case '>':
                        predicate = function (date) {
                            return date > targetDate;
                        };
                        break;
                    default: // '='
                        predicate = function (date) {
                            return date.getTime() === targetDate.getTime();
                        };
                }
                predicates.push(predicate);
            }

            // Return a predicate function that applies all conditions.
            return function (inputDate) {
                let date = (inputDate instanceof Date) ? inputDate : new Date(inputDate);
                if (isNaN(date.getTime())) {
                    return false;
                }
                return predicates.every(fn => fn(date));
            };
        },

        /**
         * 將字串中的特殊字符轉義。
         * @param string {String} - 字串
         * @returns {String} - 轉義後的字串
         */
        escapeRegex: function (string) {
            return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
        },

        /**
         * 建立正規表達式。
         * @param string {String} - 字串
         * @returns {RegExp} - 正規表達式
         */
        urlRegex: function (string) {
            // 給定一個網域和路徑,正規表達式會尋找符合以下規則的子字串:
            //  - 以 http:// 或 https:// 開頭
            //  - 網域前不能有額外的 /
            //  - 網域緊接在 :// 之後,或前面有 .(以考慮子網域)
            //  - 網域和路徑之後,符合以下其中一項:
            //     - 字串結束
            //     - 下一個字元是 /
            //     - 網域以句點結尾(允許像 gov.uk 這樣的頂級域名)
            return new RegExp('https?:\\/\\/([^\\/]*\\.)?' + CiteUnseen.escapeRegex(string) + '($|((?<=\\.)|\\/))');
        },

        /**
         * 檢查來源的作者是否符合規則。
         * @param coins {Object} - COinS object
         * @param rule {Object} - 規則
         * @returns {boolean} - 是否符合規則
         */
        matchAuthor: function (coins, rule) {
            let author = coins['rft.au'];  // can be a string or an array
            if (coins['rft.aulast']) {
                let appendedAuthors = coins['rft.aulast'];
                if (typeof appendedAuthors === 'string' && coins['rft.aufirst']) {
                    appendedAuthors = [coins['rft.aufirst'] + ' ' + appendedAuthors];
                } else if (coins['rft.aufirst']) {
                    for (let i = 0; i < coins['rft.aufirst'].length; i++) {
                        appendedAuthors[i] = coins['rft.aufirst'][i] + ' ' + appendedAuthors[i];
                    }
                }
                if (typeof author === 'string') {
                    author = [author];
                }
                author.concat(appendedAuthors);
            }
            if (!author || !rule['author']) {
                return false;
            }
            let authorRegex = new RegExp(rule['author'], 'i');
            if (typeof author === 'string') {
                return authorRegex.test(author);
            } else {
                for (let au of author) {
                    if (authorRegex.test(au)) {
                        return true;
                    }
                }
                return false;
            }
        },

        /**
         * 檢查來源的出版商是否符合規則。
         * @param coins {Object} - COinS object
         * @param rule {Object} - 規則
         * @returns {boolean} - 是否符合規則
         */
        matchPublisher: function (coins, rule) {
            const coinsPub = coins['rft.pub'] || coins['rft.jtitle'] || null;
            if (!coinsPub || !rule['pub']) {
                return false;
            }
            let publisherRegex = new RegExp(rule['pub'], 'i');
            if (typeof coinsPub === 'string') {
                return publisherRegex.test(coinsPub);
            } else {
                for (let publisher of coinsPub) {
                    if (publisherRegex.test(publisher)) {
                        return true;
                    }
                }
                return false;
            }
        },

        /**
         * 檢查 COinS 物件中的日期是否符合日期規則。
         * @param coins {Object} - COinS 物件。
         * @param rule {Object} - 包含日期規則字串的規則物件。
         * @returns {boolean} - 如果日期符合規則則返回 true,否則返回 false。
         */
        matchDate: function (coins, rule) {
            let predicate = CiteUnseen.parseDateRule(rule['date']);
            if (!predicate) {
                console.log("[Cite Unseen] Invalid date rule: " + rule['date']);
                return false;
            }

            return predicate(coins['rft.date']);
        },

        /**
         * 檢查來源的 URL 是否符合規則。
         * @param coins {Object} - COinS object
         * @param rule {Object} - 規則
         * @returns {boolean} - 是否符合規則
         */
        matchUrl: function (coins, rule) {
            if (!rule['url'] || !coins['rft_id']) {
                return false;
            }
            let urlRegex = CiteUnseen.urlRegex(rule['url']);
            return urlRegex.test(coins['rft_id']);
        },

        /**
         * 檢查來源的 URL 字串是否符合規則。
         * @param coins {Object} - COinS object
         * @param rule {Object} - 規則
         * @returns {boolean} - 是否符合規則
         */
        matchUrlString: function (coins, rule) {
            if (!rule['url_str'] || !coins['rft_id']) {
                return false;
            }
            let urlString = rule['url_str'];
            if (typeof urlString === 'string') {
                return coins['rft_id'].includes(urlString);
            } else {
                for (let str of urlString) {
                    if (coins['rft_id'].includes(str)) {
                        return true;
                    }
                }
                return false;
            }
        },

        /**
         * 檢查來源是否符合規則。
         * @param coins {Object} - COinS object
         * @param rule {Object} - 規則
         * @returns {boolean} - 是否符合規則
         */
        match: function (coins, rule) {
            if (!rule) {
                console.log("[Cite Unseen] There are empty rules in the ruleset.");
                return false;
            }
            const matchFunctions = {
                'author': CiteUnseen.matchAuthor,
                'pub': CiteUnseen.matchPublisher,
                'date': CiteUnseen.matchDate,
                'url': CiteUnseen.matchUrl,
                'url_str': CiteUnseen.matchUrlString,
            };
            for (let key of Object.keys(rule)) {
                if (!matchFunctions[key]) {
                    console.log("[Cite Unseen] Unknown rule:");
                    console.log(rule);
                    continue;
                }
                if (!matchFunctions[key](coins, rule)) {
                    return false;
                }
            }
            return true;
        },

        /**
         * 為引用的來源加上圖示。僅在頁面載入時執行一次。
         */
        addIcons: function () {
            // 將 categorizedRules 過濾為僅包含頁面引用中出現的連結
            // 考慮到我們追蹤的網域數量以及後續使用的正規表達式,這樣可以顯著節省時間
            // 在一篇約有 500 個引用的文章上進行快速測試,速度提升約 5 倍
            let filteredCategorizedRules = {};
            for (let key of Object.keys(CiteUnseen.categorizedRules)) {
                filteredCategorizedRules[key] = [];
                for (let link of CiteUnseen.refLinks) {
                    for (let rule of CiteUnseen.categorizedRules[key]) {
                        let domain = rule['url'];
                        if (!(CiteUnseen.citeUnseenDomainIgnore[key] && CiteUnseen.citeUnseenDomainIgnore[key].includes(domain)) && link.includes(domain)) {
                            if (!filteredCategorizedRules[key].includes(rule)) {
                                filteredCategorizedRules[key].push(rule);
                            }
                        }
                    }
                }
            }
            let typeCategories = CiteUnseen.citeUnseenCategoryTypes.flatMap(x => x[1]);

            // 來源是否未被標記
            let unknownSet;

            CiteUnseen.refs.forEach(function (ref) {
                // 在 <cite> 標籤前加入圖示區域
                let iconsDiv = document.createElement("div");
                iconsDiv.classList.add("cite-unseen-icons");
                iconsDiv.style.display = 'inline-flex';
                iconsDiv.style.gap = '0 5px';
                iconsDiv.style.paddingRight = '5px';
                iconsDiv.style.verticalAlign = 'middle';
                ref.cite.prepend(iconsDiv);

                if (window.cite_unseen_dashboard) {
                    // 提前放入自訂來源分類的圖示
                    let iconNode = CiteUnseen.processIcon(iconsDiv, "flag");
                    iconNode.style.display = 'none';  // 預設隱藏
                    CiteUnseen.customCategoryIcons.push(iconNode);
                }

                // 根據 class 名稱判斷來源類型。
                const classList = ref.cite.classList;
                if (classList.contains("book") || classList.contains("journal") || classList.contains("encyclopaedia") || classList.contains("conference") || classList.contains("thesis") || classList.contains("magazine")) {
                    if (CiteUnseen.citeUnseenCategories.books) {
                        CiteUnseen.processIcon(iconsDiv, "books");
                    }
                } else if (classList.contains("pressrelease")) {
                    if (CiteUnseen.citeUnseenCategories.press) {
                        CiteUnseen.processIcon(iconsDiv, "press");
                    }
                } else if (classList.contains("episode") || classList.contains("podcast") || classList.contains("media")) {
                    if (CiteUnseen.citeUnseenCategories.tvPrograms) {
                        CiteUnseen.processIcon(iconsDiv, "tvPrograms");
                    }
                } else if (ref.coins['rft_id']) {
                    unknownSet = true;

                    // 來源檢查表
                    let checked = false;
                    for (let checklistTypeData of CiteUnseen.citeUnseenChecklists) {
                        if (checked) {
                            break;
                        }
                        let checklistType = checklistTypeData[0];
                        for (let checklist of checklistTypeData[1]) {
                            if (checked) {
                                break;
                            }
                            let checklistName = checklist[0], checklistID = checklist[1];
                            for (let rule of filteredCategorizedRules[checklistID]) {
                                if (CiteUnseen.match(ref.coins, rule)) {
                                    if (CiteUnseen.citeUnseenCategories[checklistID]) {
                                        CiteUnseen.processIcon(iconsDiv, checklistType, checklistName);
                                        checked = true;
                                        unknownSet = false;
                                        break;
                                    }
                                    unknownSet = false;
                                }
                            }
                        }
                    }

                    // 類型
                    for (let category of typeCategories) {
                        for (let rule of filteredCategorizedRules[category]) {
                            if (CiteUnseen.match(ref.coins, rule)) {
                                if (CiteUnseen.citeUnseenCategories[category]) {
                                    CiteUnseen.processIcon(iconsDiv, category);
                                    unknownSet = false;
                                    break;
                                }
                                unknownSet = false;
                            }
                        }
                    }

                    if (CiteUnseen.citeUnseenCategories.unknown && unknownSet) {
                        // 並未被識別的來源,標記為未知。
                        CiteUnseen.processIcon(iconsDiv, "unknown");
                    }
                }
            });

            if (window.cite_unseen_dashboard) {
                CiteUnseen.showDashboard();
            }

            // 結束計時
            console.timeEnd('CiteUnseen runtime');
        },

        /**
         * 添加計數。目前無論是否在 reflist 中,都會記錄進去。
         * @param node {Element} - 節點
         * @param type {String} - 類型
         */
        addToCount: function (node, type) {
            CiteUnseen.citeUnseenCategoryData[type].count = CiteUnseen.citeUnseenCategoryData[type].count + 1;
        },

        /**
         * 為節點添加圖示、滑鼠懸浮文字。
         * @param node {Element} - iconsDiv 節點
         * @param type {String} - 類型
         * @param checklist {String|null} - 檢查表
         * @returns {Element} - iconNode 節點
         */
        processIcon: function (node, type, checklist = null) {
            let iconNode = document.createElement("img");
            iconNode.classList.add("skin-invert");
            iconNode.classList.add("cite-unseen-icon-" + type);
            iconNode.setAttribute("src", CiteUnseen.citeUnseenCategoryData[type].icon);
            let message = CiteUnseen.convByVar({
                hant: CiteUnseen.citeUnseenCategoryData[type].hint_hant,
                hans: CiteUnseen.citeUnseenCategoryData[type].hint_hans,
                en: CiteUnseen.citeUnseenCategoryData[type].hint_en,
                ja: CiteUnseen.citeUnseenCategoryData[type].hint_ja,
            });
            if (checklist) {
                message = CiteUnseen.convByVar({
                    hant: '來自 ',
                    hans: '来自 ',
                    en: 'From ',
                    ja: '出典 ',
                }) + checklist + CiteUnseen.convByVar({
                    hant: ':',
                    hans: ':',
                    en: ': ',
                    ja: ':',
                }) + message + CiteUnseen.convByVar({
                    hant: '點擊圖示可打開檢查表頁面以查看詳情。',
                    hans: '点击图标可打开检查表页面以查看详情。',
                    en: ' Click the icon to open the checklist page to view details.',
                    ja: 'アイコンをクリックすると、チェックリストページを開いて詳細を確認できます。',
                });
            }
            iconNode.setAttribute("alt", message);
            iconNode.setAttribute("title", "[Cite Unseen] " + message);
            CiteUnseen.addToCount(node, type);
            iconNode.style.width = "17px";
            iconNode.style.height = "17px";
            iconNode.style.objectFit = 'contain';
            iconNode.style.cssText += 'max-width: 17px !important;';
            if (checklist) {
                // 如果有檢查表,則將圖示包裝在連結中。
                const iconNodeLink = document.createElement("a");
                iconNodeLink.setAttribute("href", "//zh.wikipedia.org/wiki/" + checklist);
                iconNodeLink.setAttribute("target", "_blank");
                iconNodeLink.style.display = "contents";
                iconNodeLink.appendChild(iconNode);
                node.appendChild(iconNodeLink);
            } else {
                node.appendChild(iconNode);
            }
            if (!CiteUnseen.refCategories[type]) {
                CiteUnseen.refCategories[type] = [];
            }
            CiteUnseen.refCategories[type].push(node.parentNode);
            return iconNode;
        },

        CustomCategoryDialog: function (config) {
            CiteUnseen.CustomCategoryDialog.super.call(this, config);
        },

        /**
         * 建立一個選擇「網址」或「網址字串」的列。
         * 此函式會生成一個下拉選單,允許使用者在「網址」與「網址字串」之間切換,
         * 並根據選擇更新規則物件。
         * @param {Object} rule - 要根據選擇進行更新的規則物件。
         */
        createUrlRow: function (rule) {
            const chineseUrl = CiteUnseen.convByVar({
                hant: '網址',
                hans: '网址',
                en: 'URL',
                ja: 'URL',
            });
            const chineseUrlString = CiteUnseen.convByVar({
                hant: '網址字串',
                hans: '网址字符串',
                en: 'URL String',
                ja: 'URL 文字列',
            });
            // Create menu options for URL and URL String using convByVar.
            var optionURL = new OO.ui.MenuOptionWidget({
                data: 'url', label: chineseUrl,
            });
            var optionURLString = new OO.ui.MenuOptionWidget({
                data: 'url_str', label: chineseUrlString,
            });
            // Initialize the dropdown label based on the current rule.
            var initialLabel = rule.hasOwnProperty('url_str') ? chineseUrlString : chineseUrl;
            var dropdown = new OO.ui.DropdownWidget({
                label: initialLabel, menu: {
                    items: [optionURL, optionURLString],
                },
            });
            // Update the rule when an option is chosen.
            dropdown.getMenu().on('choose', function (item) {
                var data = item.getData();
                if (data === 'url') {
                    if (rule.url_str !== undefined) {
                        rule.url = rule.url_str;
                        delete rule.url_str;
                    }
                    dropdown.setLabel(chineseUrl);
                } else if (data === 'url_str') {
                    if (rule.url !== undefined) {
                        rule.url_str = rule.url;
                        delete rule.url;
                    }
                    dropdown.setLabel(chineseUrlString);
                }
            });
            // Create the URL text input widget.
            var urlValue = rule.url || rule.url_str || '';
            var urlInput = new OO.ui.TextInputWidget({
                value: urlValue, title: CiteUnseen.convByVar({
                    hant: '網址應包含網域名,不需要包含 http(s);網址字串則可以包含任何字元。',
                    hans: '网址应包含网域名,不需要包含 http(s);网址字符串则可以包含任何字符。',
                    en: 'The URL should include the domain name, without http(s); the URL String can contain any characters.',
                    ja: 'URL にはドメイン名を含め、http(s) は不要です。URL 文字列は任意の文字を含むことができます。',
                }),
            });
            // Update rule when the text input value changes.
            urlInput.on('change', function (value) {
                if (rule.hasOwnProperty('url')) {
                    rule.url = value;
                } else {
                    rule.url_str = value;
                }
            });
            var $row = $('<div>').addClass('cite-unseen-dialog-url-row');
            $row.append(dropdown.$element, urlInput.$element);
            return $row;
        },

        /**
         * 建立可選參數的行(例如作者、出版、日期)。
         * 此函式會生成一個包含多個可選參數的區域,每個參數都包含一個勾選框和文字輸入框。
         * 當勾選框被選中時,對應的文字輸入框會啟用,否則會被禁用。
         * @param rule {Object} - 包含參數值的規則物件。
         */
        createOptionalParamsRow: function (rule) {
            var $paramsContainer = $('<div>').addClass('cite-unseen-dialog-optional-params');
            const params = ['author', 'pub', 'date'];
            const chineseParams = {
                'author': CiteUnseen.convByVar({
                    hant: '作者',
                    hans: '作者',
                    en: 'Author',
                    ja: '著者'
                }),
                'pub': CiteUnseen.convByVar({
                    hant: '出版',
                    hans: '出版',
                    en: 'Publisher',
                    ja: '出版社'
                }),
                'date': CiteUnseen.convByVar({
                    hant: '日期',
                    hans: '日期',
                    en: 'Date',
                    ja: '日付'
                }),
            };
            params.forEach(function (param) {
                var paramValue = rule[param] || '';
                var checkbox = new OO.ui.CheckboxInputWidget({
                    selected: !!paramValue,
                });
                var textInput = new OO.ui.TextInputWidget({
                    value: paramValue,
                });
                textInput.$element.css('display', !!paramValue ? 'inline-block' : 'none');
                textInput.setDisabled(!checkbox.isSelected());
                checkbox.on('change', function (isSelected) {
                    textInput.setDisabled(!isSelected);
                    textInput.$element.css('display', isSelected ? 'inline-block' : 'none');
                    if (!isSelected) {
                        textInput.setValue('');
                        delete rule[param];
                    }
                });
                textInput.on('change', function (value) {
                    rule[param] = value;
                });
                var $paramRow = $('<div>')
                    .addClass('cite-unseen-dialog-param-container')
                    .css({ 'margin-top': '5px' });
                $paramRow.append(checkbox.$element, $('<span>').text(chineseParams[param]), textInput.$element);
                $paramsContainer.append($paramRow);
            });
            return $paramsContainer;
        },

        /**
         * 為一條規則建立完整的元件。
         * 此函式會生成一個包含規則的容器,並附加相關的 UI 元件。
         * @param {Object} rule - 包含規則資料的物件。
         * @param {OO.ui.FieldsetLayout} parentFieldset - 父級的 Fieldset,用於移除規則時更新 UI。
         */
        createRuleWidget: function (rule, parentFieldset) {
            var $container = $('<div>').addClass('cite-unseen-dialog-rule-container');
            var $urlRow = CiteUnseen.createUrlRow(rule);
            var $optionalParams = CiteUnseen.createOptionalParamsRow(rule);
            var ruleWidget = new OO.ui.Widget({ $element: $container });
            // Attach rule data directly to the widget.
            ruleWidget.ruleData = rule;
            var removeButton = new OO.ui.ButtonWidget({
                icon: 'trash', flags: ['destructive'],
            });
            removeButton.$element.addClass('cite-unseen-dialog-remove-button');
            removeButton.on('click', function () {
                if (parentFieldset) {
                    parentFieldset.removeItems([ruleWidget]); // Properly remove from the fieldset.
                } else {
                    ruleWidget.$element.remove();
                }
                CiteUnseen.updateCustomCategoryDialogHeight();
            });
            $container.append($urlRow, $optionalParams, removeButton.$element);
            return ruleWidget;
        },

        /**
         * 生成自訂來源分類對話框的內容。
         * @returns {OO.ui.FieldsetLayout} - 自訂來源分類對話框的內容
         */
        generateCustomCategoryDialogRules: function (initialRules) {
            var fieldset = new OO.ui.FieldsetLayout({
                classes: ['cite-unseen-dialog-rules'],
            });
            if (initialRules) {
                // Create a widget for each rule without updating any global array.
                initialRules.forEach(function (rule) {
                    fieldset.addItems([CiteUnseen.createRuleWidget(rule, fieldset)]);
                });
            }
            var addButton = new OO.ui.ButtonWidget({
                label: CiteUnseen.convByVar({
                    hant: '新增規則',
                    hans: '新增规则',
                    en: 'Add Rule',
                    ja: 'ルールを追加',
                }), icon: 'add',
            });
            var addButtonField = new OO.ui.FieldLayout(addButton, { align: 'center' });
            addButtonField.$element.addClass('cite-unseen-dialog-add-button');
            fieldset.addButtonField = addButtonField;
            addButton.on('click', function () {
                var newRule = { url: '' };
                var newWidget = CiteUnseen.createRuleWidget(newRule, fieldset);
                // Insert new rule widget just before the add button.
                fieldset.removeItems([fieldset.addButtonField]);
                fieldset.addItems([newWidget]);
                fieldset.addItems([fieldset.addButtonField]);
                CiteUnseen.updateCustomCategoryDialogHeight();
            });
            fieldset.addItems([addButtonField]);
            return fieldset;
        },

        /**
         * 更新自訂來源分類對話框的高度。
         */
        updateCustomCategoryDialogHeight: function () {
            if (CiteUnseen.customCategoryDialog) {
                var maxHeight = $(window).height() * 0.5;
                CiteUnseen.customCategoryDialog.panel.$element.css('max-height', maxHeight + 'px');
                CiteUnseen.customCategoryDialog.updateSize();
            }
        },

        /**
         * 初始化自訂來源分類對話框。
         */
        initCustomCategoryDialog: function () {
            if (CiteUnseen.CustomCategoryDialogRules === null) {
                CiteUnseen.CustomCategoryDialogRules = new OO.ui.FieldsetLayout();
            }
            OO.inheritClass(CiteUnseen.CustomCategoryDialog, OO.ui.Dialog);
            CiteUnseen.CustomCategoryDialog.static.name = 'CustomCategoryDialog';

            CiteUnseen.CustomCategoryDialog.prototype.initialize = function () {
                CiteUnseen.CustomCategoryDialog.super.prototype.initialize.apply(this, arguments);

                this.panel = new OO.ui.PanelLayout({
                    padded: true, expanded: false,
                });
                this.panel.$element.addClass('cite-unseen-dialog-panel');
                this.content = new OO.ui.FieldsetLayout();
                this.content.$element.addClass('cite-unseen-dialog-content');

                // 標題
                this.titleLabelWidget = new OO.ui.LabelWidget({
                    label: CiteUnseen.convByVar({
                        hant: '[Cite Unseen] 添加自訂來源分類規則',
                        hans: '[Cite Unseen] 添加自定义来源分类规则',
                        en: '[Cite Unseen] Add Custom Citation Category Rules',
                        ja: '[Cite Unseen] カスタム引用カテゴリルールを追加',
                    }), classes: ['cite-unseen-dialog-title'],
                });
                this.titleLabelWidget.$element.append($('<span>')
                    .text(CiteUnseen.convByVar({
                        hant: '(此為臨時生效;若需自訂已有的來源分類,請參見「',
                        hans: '(此为临时生效;若需自定义已有的来源分类,请参见「',
                        en: ' (This is temporary; for customizing existing citation categories, see "',
                        ja: '(これは一時的なものであり、既存の引用カテゴリをカスタマイズするには「',
                    }))
                    .css({ 'font-size': '0.75em', 'font-weight': 'normal' })
                    .append($('<a>')
                        .text(CiteUnseen.convByVar({
                            hant: '進階自訂教程',
                            hans: '进阶自定义教程',
                            en: 'Customization Guide',
                            ja: 'カスタマイズガイド',
                        }))
                        .attr('href', '//zh.wikipedia.org/wiki/User:SuperGrey/gadgets/CiteUnseen'))
                    .append(CiteUnseen.convByVar({
                        hant: '」。)',
                        hans: '」。)',
                        en: '.")',
                        ja: '」。)',
                    }))
                );

                // 動作按鈕
                this.cancelButton = new OO.ui.ButtonWidget({
                    label: CiteUnseen.convByVar({
                        hant: '取消',
                        hans: '取消',
                        en: 'Cancel',
                        ja: 'キャンセル',
                    }), flags: ['safe', 'close'], action: 'cancel',
                });
                this.saveButton = new OO.ui.ButtonWidget({
                    label: CiteUnseen.convByVar({
                        hant: '儲存',
                        hans: '保存',
                        en: 'Save',
                        ja: '保存',
                    }),
                    flags: ['primary', 'progressive'],
                    action: 'save',
                });
                this.actionButtons = new OO.ui.ButtonGroupWidget({
                    items: [
                        this.cancelButton, this.saveButton,
                    ],
                });
                this.actionButtons.$element.addClass('cite-unseen-dialog-action-buttons');

                // 為按鈕添加點擊事件
                this.cancelButton.$element.on('click', function (e) {
                    this.close();
                }.bind(this));
                this.saveButton.$element.on('click', async function (e) {
                    let response = await CiteUnseen.addCustomCategory();
                    if (response) {
                        this.close();
                    }
                }.bind(this));

                // 視窗內容
                this.ruleContent = CiteUnseen.generateCustomCategoryDialogRules(CiteUnseen.customCategoryRules);
                this.content.addItems([
                    this.titleLabelWidget, this.actionButtons, this.ruleContent,
                ]);

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

                // Automatically update height on window resize.
                $(window).on('resize.customCategoryDialog', function () {
                    CiteUnseen.updateCustomCategoryDialogHeight();
                });
                CiteUnseen.updateCustomCategoryDialogHeight();
            };
            CiteUnseen.CustomCategoryDialog.prototype.getBodyHeight = function () {
                return this.panel.$element.outerHeight(true);
            };
            CiteUnseen.CustomCategoryDialog.prototype.getTearDownProcess = function (data) {
                return CiteUnseen.CustomCategoryDialog.super.prototype.getTearDownProcess.call(this, data);
            };
        },

        /**
         * 添加自訂來源分類規則。
         * @returns {Promise<boolean>} - 返回一個 Promise,表示操作是否成功。
         */
        addCustomCategory: async function () {
            if (!CiteUnseen.customCategoryDialog) {
                console.error("[Cite Unseen] customCategoryDialog is not initialized when addCustomCategory is called.");
                return false;
            }
            var dialog = CiteUnseen.customCategoryDialog;
            var newRules = [];
            dialog.ruleContent.getItems().forEach(function (item) {
                // Exclude the add button field and only collect widgets with ruleData.
                if (item !== dialog.ruleContent.addButtonField && item.ruleData) {
                    newRules.push(item.ruleData);
                }
            });
            console.log(newRules);

            for (let i = 0; i < newRules.length; i++) {
                // Ensure the rule has a non-empty URL value.
                var rule = newRules[i];
                var url = rule.url || rule.url_str;
                if (!url || url.trim() === '') {
                    mw.notify(CiteUnseen.convByVar({
                        hant: '來源網址無效,請檢查並重新輸入。',
                        hans: '来源网址无效,请检查并重新输入。',
                        en: 'Invalid source URL, please check and re-enter.',
                        ja: '無効なソースURLです。確認して再入力してください。',
                    }), { type: 'error', autoHide: true, title: '[Cite Unseen]' });
                    return false;
                }
            }

            CiteUnseen.customCategoryRules = newRules;

            // 根據自訂規則更新來源圖示的 display 狀態。
            let customCategoryCitations = [];
            for (let i = 0; i < CiteUnseen.refs.length; i++) {
                let ref = CiteUnseen.refs[i];
                let coins = ref.coins;
                let iconNode = CiteUnseen.customCategoryIcons[i];
                let matched = false;
                for (let rule of CiteUnseen.customCategoryRules) {
                    if (CiteUnseen.match(coins, rule)) {
                        iconNode.style.display = 'inline-block';
                        matched = true;
                        customCategoryCitations.push(ref.cite);
                        break;
                    }
                }
                if (!matched) {
                    iconNode.style.display = 'none';
                }
            }

            // 高亮顯示來源
            CiteUnseen.highlightCitation('flag', customCategoryCitations);
            CiteUnseen.refCategories['flag'] = customCategoryCitations;

            return true;
        },

        /**
         * 顯示自訂來源分類對話框。
         */
        showCustomCategoryDialog: function () {
            if (CiteUnseen.CustomCategoryDialogRules === null) {
                CiteUnseen.initCustomCategoryDialog();
            }
            CiteUnseen.CustomCategoryDialogRules.clearItems();
            CiteUnseen.customCategoryDialog = new CiteUnseen.CustomCategoryDialog({ padded: true, scrollable: true });
            CiteUnseen.CustomCategoryDialogRules.addItems(CiteUnseen.generateCustomCategoryDialogRules());

            if (CiteUnseen.windowManager === null) {
                CiteUnseen.windowManager = new OO.ui.WindowManager({ modal: false, classes: ['cite-unseen-non-modal'] });
                mw.util.addCSS(`
            .cite-unseen-non-modal {
                position: -webkit-sticky;
                position: sticky;
                bottom: 1em;
                max-width: 960px;
                margin-left: auto;
                margin-right: auto;
                background-color: white;
            }
            .cite-unseen-non-modal.oo-ui-windowManager-size-full {
                width: 100%;
                height: 100%;
                bottom: 0;
            }
            .cite-unseen-non-modal .oo-ui-window {
                border: 1px solid #a2a9b1;
                box-shadow: 0 0 4px 0 rgba( 0, 0, 0, 0.25 );
            }
            .cite-unseen-non-modal.oo-ui-windowManager-size-full .oo-ui-window {
                border: 0;
                box-shadow: unset;
            }
            .cite-unseen-dialog-panel {
                padding-bottom: 0;
            }
            .cite-unseen-dialog-action-buttons {
                position: absolute;
                top: 0;
                right: -5px;
                display: flex;
                gap: 0 10px;
                flex-wrap: wrap;
            } 
            .cite-unseen-dialog-title {
                font-size: 1.12em;
                font-weight: bold;
                margin-bottom: 10px;
                margin-right: 127px;
            }
            .cite-unseen-dialog-content {
                padding-bottom: 16px;
            }
            .cite-unseen-dialog-rule-container {
                position: relative;
                width: 20em;
                padding: 10px;
                background-color: rgb(249, 249, 249);
                border: 1px solid rgb(204, 204, 204);
                flex-grow: 1;
            }
            .cite-unseen-dialog-rules .oo-ui-fieldsetLayout-group {
                display: flex;
                flex-direction: row;
                flex-wrap: wrap;
                gap: 10px;
            }
            .cite-unseen-dialog-url-row {
                display: flex;
                align-items: center;
                margin-right: 55px;
            }
            .cite-unseen-dialog-url-row .oo-ui-dropdownWidget {
                width: auto !important;
                display: inline-block;
                vertical-align: middle;
                margin-right: 0;
            }
            .cite-unseen-dialog-url-row .oo-ui-textInputWidget {
                max-width: 100%;
                display: inline-block;
                vertical-align: middle;
                margin-left: 8px;
            }
            .cite-unseen-dialog-optional-params {
                display: flex;
                align-items: center;
                flex-wrap: wrap;
                flex-direction: row;
                gap: 0 10px;
            }
            .cite-unseen-dialog-param-container {
                display: flex;
                align-items: center;
                flex-grow: 1;
            }
            .cite-unseen-dialog-param-container .oo-ui-textInputWidget {
                width: 10em;
                vertical-align: middle;
                margin-left: 8px;
                flex-grow: 1;
            }
            .cite-unseen-dialog-remove-button {
                display: block;
                position: absolute;
                top: 11px;
                right: 11px;
            }
            .cite-unseen-dialog-add-button {
                margin-top: 0;
            }
            `);
                $('body').append(CiteUnseen.windowManager.$element);
            }
            CiteUnseen.windowManager.addWindows({ 'customCategoryDialog': CiteUnseen.customCategoryDialog });
            CiteUnseen.windowManager.openWindow('customCategoryDialog');
        },

        /**
         * 解析帶複數記號 "(s)" 的字串
         * @param {string} string - 要解析的字串
         * @param {number} value - 用於判斷複數形式的值
         * @return {string} - 返回解析後的字串
         */
        parseI18nPlural: function (string, value) {
            return string.replace(/\(s\)/g, value === 1 ? '' : 's');
        },

        /**
         * 添加來源統計結果展板。
         */
        showDashboard: function () {
            if (CiteUnseen.refs.length === 0) {
                // 沒有來源,不顯示展板。
                return;
            }

            if (CiteUnseen.dashboard === null) {
                CiteUnseen.dashboard = {
                    div: document.createElement('div'),
                    total: document.createElement('div'),
                    cats: document.createElement('div'),
                    flag: document.createElement('a'),
                    custom: false,
                };

                CiteUnseen.dashboard.div.style.border = '1px solid #ccc';
                CiteUnseen.dashboard.div.style.marginBottom = '1em';
                CiteUnseen.dashboard.div.style.borderRadius = '5px';
                CiteUnseen.dashboard.div.style.fontSize = '.8em';
                CiteUnseen.dashboard.div.style.display = 'flex';
                CiteUnseen.dashboard.div.style.gap = '.5em 1em';
                CiteUnseen.dashboard.div.style.flexWrap = 'wrap';
                CiteUnseen.dashboard.div.style.padding = '5px';
                CiteUnseen.dashboard.div.style.justifyContent = 'center';
                CiteUnseen.dashboard.div.style.textAlign = 'center';

                // 標記的來源總數
                let refLength = CiteUnseen.refs.length;
                CiteUnseen.dashboard.total.innerText = "[Cite Unseen] " + CiteUnseen.convByVar({
                    hant: "共 ",
                    hans: "共 ",
                    en: "Total ",
                    ja: "合計 ",
                }) + refLength + CiteUnseen.convByVar({
                    hant: ' 個來源',
                    hans: ' 个来源',
                    en: ' citation' + (refLength > 1 ? 's' : ''),
                    ja: ' 件の引用',
                });
                CiteUnseen.dashboard.total.style.fontWeight = 'bold';
                CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.total);

                // 各類型來源
                CiteUnseen.dashboard.cats.style.display = 'contents';
                CiteUnseen.dashboard.cats.style.textAlign = 'center';
                CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.cats);

                // 自訂來源分類按鈕
                CiteUnseen.dashboard.flag.innerText = CiteUnseen.convByVar({
                    hant: '自訂',
                    hans: '自定义',
                    en: 'Custom',
                    ja: 'カスタム',
                });
                CiteUnseen.dashboard.flag.style.cursor = 'pointer';
                const flagIcon = document.createElement('img');
                flagIcon.src = CiteUnseen.citeUnseenCategoryData['flag'].icon;
                flagIcon.style.width = '17px';
                flagIcon.style.height = '17px';
                flagIcon.style.objectFit = 'contain';
                flagIcon.style.cssText += 'max-width: 17px !important;';
                flagIcon.style.marginRight = '5px';
                flagIcon.style.verticalAlign = 'middle';
                flagIcon.style.display = 'inline-block';
                this.dashboard.flag.prepend(flagIcon);

                CiteUnseen.dashboard.flag.onclick = function () {
                    CiteUnseen.showCustomCategoryDialog();
                };
                CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.flag);

                mw.util.addCSS(`
            .cite-unseen-highlight {
                background-color: rgba(255, 246, 153, 0.5);
                -webkit-box-decoration-break: clone;
                box-decoration-break: clone;
            }
            `);

                // 在 {{reflist}} 之前插入展板。若無 {{reflist}},則插入在末尾。
                document.querySelector('#mw-content-text .mw-parser-output').insertBefore(CiteUnseen.dashboard.div, CiteUnseen.reflistNode);
            }

            // 清空各類型來源
            CiteUnseen.dashboard.cats.innerHTML = '';

            // 按順序列出各類型來源
            let categoryTypes = CiteUnseen.citeUnseenChecklists.flatMap(x => x[0]);
            categoryTypes = categoryTypes.concat(CiteUnseen.citeUnseenCategoryTypes.flatMap(x => x[1]));
            categoryTypes.push('unknown');
            for (let category of categoryTypes) {
                let categoryData = CiteUnseen.citeUnseenCategoryData[category];
                if (categoryData.count > 0) {
                    let countNode = document.createElement('div');
                    let countIcon = document.createElement('img');
                    countIcon.alt = CiteUnseen.convByVar({
                        hant: categoryData.hint_hant,
                        hans: categoryData.hint_hans,
                        en: categoryData.hint_en,
                        ja: categoryData.hint_ja,
                    });
                    countIcon.src = categoryData.icon;
                    countIcon.width = '17';
                    countIcon.style.maxHeight = '18px';
                    let countText = document.createElement('span');
                    countText.innerText = categoryData.count + ' ' + CiteUnseen.convByVar({
                        hant: categoryData.label_hant,
                        hans: categoryData.label_hans,
                        en: CiteUnseen.parseI18nPlural(categoryData.label_en, categoryData.count),
                        ja: categoryData.label_ja,
                    });
                    countText.style.paddingLeft = '5px';
                    countText.style.cursor = 'pointer';
                    countText.onmouseover = function () {
                        countText.style.textDecoration = 'underline';
                    };
                    countText.onmouseout = function () {
                        countText.style.textDecoration = 'none';
                    };
                    countText.onclick = function () {
                        if (CiteUnseen.dashboardHighlighted === category) {
                            CiteUnseen.highlightCitation(null);  // 取消高亮
                        } else {
                            CiteUnseen.highlightCitation(category);
                        }
                    };
                    countNode.appendChild(countIcon);
                    countNode.appendChild(countText);
                    CiteUnseen.dashboard.cats.appendChild(countNode);
                }
            }
        },

        /**
         * 高亮顯示來源。
         * @param category - 來源類別
         * @param nodes - 節點列表。預設為 null,則使用來源類別中的節點。
         */
        highlightCitation: function (category, nodes = null) {
            if (CiteUnseen.dashboardHighlighted) {
                // 清除之前的高亮
                CiteUnseen.refCategories[CiteUnseen.dashboardHighlighted].forEach(function (node) {
                    node.classList.remove('cite-unseen-highlight');
                });
            }
            CiteUnseen.dashboardHighlighted = category;
            if (category) {
                nodes = nodes || CiteUnseen.refCategories[category];  // 若未指定節點,則使用來源類別中的節點
                nodes.forEach(function (node) {
                    node.classList.add('cite-unseen-highlight');
                });
            }
        },

        /**
         * (helper function) 過濾 obj.filter === filterValue。
         * @param obj - 要過濾的 object
         * @param filter - 過濾的鍵
         * @param filterValue - 過濾的值
         * @returns {Object} - 過濾後的 object
         */
        filterObjectIncludes: function (obj, filter, filterValue) {
            return Object.keys(obj).reduce((acc, val) => (obj[val][filter] !== filterValue ? acc : {
                ...acc, [val]: obj[val],
            }), {});
        },

        /**
         * (helper function) 過濾 obj.filter !== filterValue。
         * @param obj - 要過濾的 object
         * @param filter - 過濾的鍵
         * @param filterValue - 過濾的值
         * @returns {Object} - 過濾後的 object
         */
        filterObjectExcludes: function (obj, filter, filterValue) {
            return Object.keys(obj).reduce((acc, val) => (obj[val][filter] === filterValue ? acc : {
                ...acc, [val]: obj[val],
            }), {});
        },

        /**
         * 從 User:<username>/CiteUnseen-Rules.js 導入使用者自訂規則。
         */
        importCustomRules: async function () {
            try {
                await mw.loader.getScript('/w/index.php?title=User:' + encodeURIComponent(mw.config.get('wgUserName')) + '/CiteUnseen-Rules.js&ctype=text/javascript&action=raw');
            } catch (err) {
                console.log("[Cite Unseen] Error getting Cite Unseen custom rules: " + err.message);
                return;
            }

            try {
                // 以前用過的 config 名稱
                if (!window.cite_unseen_categories && window.cite_unseen_rules) {
                    window.cite_unseen_categories = window.cite_unseen_rules;
                }
                if (!window.cite_unseen_categories && window.cite_unseen_ruleset) {
                    window.cite_unseen_categories = window.cite_unseen_ruleset;
                }

                // 獲取使用者的來源分類設定
                if (window.cite_unseen_categories && typeof window.cite_unseen_categories === 'object') {
                    for (let key in window.cite_unseen_categories) {
                        if (key in CiteUnseen.citeUnseenCategories) {
                            CiteUnseen.citeUnseenCategories[key] = window.cite_unseen_categories[key];
                        } else if (key in [
                            "blacklisted",
                            "deprecated",
                            "generallyUnreliable",
                            "marginallyReliable",
                            "generallyReliable",
                            "multi",
                        ]) {
                            for (let checklistTypeData of CiteUnseen.citeUnseenChecklists) {
                                if (checklistTypeData[0] === key) {
                                    for (let checklist of checklistTypeData[1]) {
                                        CiteUnseen.citeUnseenCategories[checklist] = window.cite_unseen_categories[key];
                                    }
                                }
                            }
                        }
                    }
                }

                // 獲取使用者的網域忽略列表
                if (window.cite_unseen_domain_ignore && typeof window.cite_unseen_domain_ignore === 'object') {
                    for (let key in window.cite_unseen_domain_ignore) {
                        if (window.cite_unseen_domain_ignore[key].length && key in CiteUnseen.citeUnseenDomainIgnore) {
                            CiteUnseen.citeUnseenDomainIgnore[key] = window.cite_unseen_domain_ignore[key];
                        }
                    }
                }

                // 獲取使用者的自訂網址列表
                if (window.cite_unseen_additional_domains && typeof window.cite_unseen_additional_domains === 'object') {
                    for (let key in window.cite_unseen_additional_domains) {
                        if (window.cite_unseen_additional_domains[key].length && key in CiteUnseen.categorizedRules) {
                            CiteUnseen.categorizedRules[key] = CiteUnseen.categorizedRules[key].concat({
                                'url': window.cite_unseen_additional_domains[key],
                            });
                        }
                    }
                }

                // 獲取使用者的自訂網址字串列表
                if (window.cite_unseen_additional_strings && typeof window.cite_unseen_additional_strings === 'object') {
                    for (let key in window.cite_unseen_additional_strings) {
                        if (window.cite_unseen_additional_strings[key].length && key in CiteUnseen.categorizedRules) {
                            CiteUnseen.categorizedRules[key] = CiteUnseen.categorizedRules[key].concat({
                                'url_str': window.cite_unseen_additional_strings[key],
                            });
                        }
                    }
                }

                // 是否顯示展板。預設顯示。
                if (window.cite_unseen_dashboard === undefined) {
                    window.cite_unseen_dashboard = true;
                }
            } catch (err) {
                console.log('[Cite Unseen] Could not read custom rules due to error: ', err);
            }
        },

        /**
         * 導入來源分類資料。
         * @returns {Promise<Record<string, Object[]>>}
         */
        importDependencies: async function () {
            if (mw.config.get('wgServer') === "//zh.wikipedia.org") {
                // 在中文維基百科,優先使用 ext.gadget.HanAssist 模組。
                await mw.loader.using('ext.gadget.HanAssist', function (require) {
                    const { convByVar } = require('ext.gadget.HanAssist');
                    CiteUnseen.convByVar = convByVar;
                });
            } else {
                let lang = mw.config.get('wgContentLanguage');
                CiteUnseen.convByVar = function (i18nDict) {
                    const locale = new Intl.Locale(lang);
                    // 如果是中文。
                    if (locale.language === 'zh') {
                        if (locale.script === 'Hans') {
                            return i18nDict['hans'] || i18nDict['hant'] || i18nDict['en'] || 'Language undefined!';
                        } else {
                            return i18nDict['hant'] || i18nDict['hans'] || i18nDict['en'] || 'Language undefined!';
                        }
                    }
                    // 其他語言。
                    return i18nDict[lang] || i18nDict['en'] || 'Language undefined!';
                };
            }

            await mw.loader.getScript('//zh.wikipedia.org/w/index.php?title=User:SuperGrey/gadgets/CiteUnseen/sources.js&action=raw&ctype=text/javascript');
            return await CiteUnseenData.getCategorizedRules();
        },

        /**
         * 尋找所有 <cite> 標籤,並將其解析為 COinS object;找到 {{reflist}} 所在之處。
         */
        findCitations: function () {
            // 结构:
            //   <cite class="citation book">...</cite>
            //   <span title="...">...</span>

            // 篩選所有 <cite> 標籤
            for (let citeTag of document.querySelectorAll("cite")) {
                let coinsObject;
                let coinsTag = citeTag.nextElementSibling;
                if (!coinsTag || coinsTag.tagName !== 'SPAN' || !coinsTag.hasAttribute('title')) {
                    // 沒有 COinS,則獲取 cite 中的 a 標籤的 href 屬性
                    // (A partial solution to parse jawiki 和書 format)
                    let aTag = citeTag.querySelector('a.external');
                    if (aTag && aTag.hasAttribute('href')) {
                        coinsObject = {
                            'rft_id': aTag.getAttribute('href'),
                        };
                    } else {
                        // 沒有 COinS,也沒有 a 標籤,則跳過
                        continue;
                    }
                } else {
                    // 解析 COinS 字串
                    let coinsString = decodeURIComponent(coinsTag.getAttribute('title'));
                    coinsObject = CiteUnseen.parseCoinsString(coinsString);
                    if (!coinsObject['rft_id']) {
                        let aTag = citeTag.querySelector('a.external');
                        if (aTag) {
                            coinsObject['rft_id'] = aTag.getAttribute('href');
                        }
                    }
                }
                CiteUnseen.refs.push({
                    cite: citeTag, coins: coinsObject,
                });
                if (coinsObject['rft_id']) {
                    CiteUnseen.refLinks.push(coinsObject['rft_id']);
                }
            }

            // 找到 {{reflist}} 所在之處。如有多個,則取最後一個。若無,則插入在末尾。
            let reflists = document.querySelectorAll('#mw-content-text .mw-parser-output div.reflist');
            if (reflists.length > 0) {
                CiteUnseen.reflistNode = reflists[reflists.length - 1];
            } else {
                CiteUnseen.reflistNode = null;  // 插入在末尾
            }
        },

        /**
         * 程式入口。
         */
        init: function () {
            // 開始計時。(在 addIcons() 結尾結束計時。)
            console.time('CiteUnseen runtime');

            // 導入來源分類資料
            CiteUnseen.importDependencies().then(function (categorizedRules) {
                CiteUnseen.categorizedRules = categorizedRules;
                CiteUnseen.citeUnseenCategories = CiteUnseenData.citeUnseenCategories;
                CiteUnseen.citeUnseenCategoryTypes = CiteUnseenData.citeUnseenCategoryTypes;
                CiteUnseen.citeUnseenChecklists = CiteUnseenData.citeUnseenChecklists;
                CiteUnseen.citeUnseenCategoryData = CiteUnseenData.citeUnseenCategoryData;

                // 補上未完備的參數
                for (let key of Object.keys(CiteUnseen.categorizedRules)) {
                    if (CiteUnseen.citeUnseenCategories[key] === undefined) {
                        CiteUnseen.citeUnseenCategories[key] = true;
                    }
                }

                // 導入使用者自訂規則
                CiteUnseen.importCustomRules().then(function () {
                    // Run on every wikipage.content hook. This is to support gadgets like QuickEdit.
                    mw.hook('wikipage.content').add(function () {
                        // If the finished loading element is still there, no need to re-run.
                        if (document.querySelector('#cite-unseen-finished-loading')) {
                            return;
                        }

                        // Process the page content
                        CiteUnseen.findCitations();
                        CiteUnseen.addIcons();

                        // Place a "Finished loading" element to the HTML.
                        let finishedLoading = document.createElement('div');
                        finishedLoading.id = 'cite-unseen-finished-loading';
                        finishedLoading.style.display = 'none';
                        document.querySelector('#mw-content-text .mw-parser-output').appendChild(finishedLoading);
                    });
                });
            });
        },

        categorizedRules: null,  // 來源分類規則
        citeUnseenCategories: null,  // 預設來源分類開關
        citeUnseenCategoryTypes: null,  // 來源分類的類型
        citeUnseenChecklists: null,  // 來源檢查表按大類區分
        citeUnseenCategoryData: null,  // 使用到的分類資料、圖示和計數
        citeUnseenDomainIgnore: {},  // 使用者自訂排除的網域
        refs: [],  // 所有參考文獻
        refLinks: [],  // 所有來源連結
        refCategories: {},  // 所有來源對應的分類
        reflistNode: null,  // {{reflist}} 所在之處
        convByVar: null,  // 簡繁轉換 & i18n
        dashboard: null,  // 展板
        dashboardHighlighted: null,  // 當前高亮的來源
        CustomCategoryDialogRules: null,  // 自訂來源分類對話框內容
        customCategoryRules: [],  // 自訂來源分類規則
        customCategoryIcons: [],  // 自訂來源分類圖示
        windowManager: null,  // Window manager for OOUI dialogs
    };

    CiteUnseen.init();

})();