ParaTranz diff

ParaTranz enhanced

当前为 2025-01-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ParaTranz diff
// @namespace    https://paratranz.cn/users/44232
// @version      0.11.1
// @description  ParaTranz enhanced
// @author       ooo
// @match        http*://paratranz.cn/*
// @icon         https://paratranz.cn/favicon.png
// @require      https://cdnjs.cloudflare.com/ajax/libs/medium-zoom/1.1.0/medium-zoom.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    // #region 主要功能函数

    // #region 自动跳过空白页 shouldSkip
    function shouldSkip() {
        if (document.querySelector('.string-list .empty-sign') &&
            location.search.match(/(\?|&)page=\d+/g)) {
            document.querySelector('.pagination .page-item a')?.click();
            return true;
        }
    }
    // #endregion

    // #region 添加快捷键 addHotkeys
    function addHotkeys() {
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.shiftKey && e.key === 'V') {
                e.preventDefault();
                mockInput(document.querySelector('.editor-core .original')?.textContent);
            }
        });
    }
    // #endregion

    // #region 更多搜索高亮 initDropMark markSearchParams initMarkJS watchContextBtn
    let markSearchParams = () => {};
    const mergeObjects = (obj1, obj2) => {
        const merged = {};
        for (const key of Object.keys(obj1)) {
            merged[key] = [...obj1[key], ...obj2[key]];
        }
        return merged;
    };
    function updMark() {
        const params = new URLSearchParams(location.search);
        const getParams = (type) => {
            return {
                contains: [...params.getAll(type)],
                startsWith: [...params.getAll(`${type}^`)],
                endsWith: [...params.getAll(`${type}$`)],
                match: [...params.getAll(`${type}~`)]
            };
        };
        const texts = getParams('text');
        const originals = getParams('original');
        const translations = getParams('translation');
        const contexts = getParams('context');

        const originKeywords = mergeObjects(texts, originals);
        const editingKeywords = mergeObjects(texts, translations);
        const contextKeywords = contexts;

        markSearchParams = () => {
            markOrigin(originKeywords);
            markContext(contextKeywords);
        }

        if (Object.values(editingKeywords).filter(v => v).length) {
            const dropMark = markEditing(editingKeywords);
            return dropMark;
        }
    }
    let dropLastTextareaMark;
    const initDropMark = () => dropLastTextareaMark = updMark();

    let originMark;
    let contextMark;

    function initMarkJS() {
        const original = document.querySelector('.editor-core .original');
        originMark = new Mark(original);

        original.addEventListener('click', (e) => {
            if (e.target.tagName === 'MARK') {
                const originalElement = e.target.parentElement;
                originalElement.click();
            }
        });

        const context = document.querySelector('.context');
        if (context) contextMark = new Mark(context);
    }

    function watchContextBtn() {
        const btn = document.querySelector('.float-right a');
        if (!btn) return;
        btn.addEventListener('click', () => {
            const context = document.querySelector('.context');
            if (!context) return;
            removeContextTags();

            const original = document.querySelector('.editor-core .original').textContent;
            contextMark = new Mark(context);
            markContext(original);
        });
    }

    function mark(target, keywords, options) {
        if (!target) return;
        target.unmark();

        const caseSensitive = !document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked;
        const flags = caseSensitive ? 'g' : 'ig';

        const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

        const { contains, startsWith, endsWith, match } = keywords;

        const patterns = [
            ...contains.map(keyword => `(${escapeRegExp(keyword)})`),
            ...startsWith.map(keyword => `^(${escapeRegExp(keyword)})`),
            ...endsWith.map(keyword => `(${escapeRegExp(keyword)})$`),
            match
        ].filter(p => p.length).join('|');

        if (patterns) {
            const regex = new RegExp(patterns, flags);
            target.markRegExp(regex, {
                acrossElements: true,
                separateWordSearch: false,
                ...options
            });
        }
    }

    function markOrigin(keywords) {
        mark(originMark, keywords);
    }

    function markContext(originTxt) {
        mark(contextMark, originTxt, { className: 'mark'});
    }

    function markEditing(keywords) {
        let textarea = document.querySelector('textarea.translation');
        if (!textarea) return;
        const lastOverlay = document.getElementById('PZSoverlay');
        if (lastOverlay) return;

        const overlay = document.createElement('div');
        overlay.id = 'PZSoverlay';
        overlay.className = textarea.className;
        const textareaStyle = window.getComputedStyle(textarea);
        for (let i = 0; i < textareaStyle.length; i++) {
            const property = textareaStyle[i];
            overlay.style[property] = textareaStyle.getPropertyValue(property);
        }
        overlay.style.position = 'absolute';
        overlay.style.pointerEvents = 'none';
        overlay.style.setProperty('background', 'transparent', 'important');
        overlay.style['-webkit-text-fill-color'] = 'transparent';
        overlay.style.overflowY = 'hidden';
        overlay.style.resize = 'none';

        textarea.parentNode.appendChild(overlay);

        const updOverlay = () => {
            overlay.innerText = textarea.value;
            const fillColor = window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color');
            mark(new Mark(overlay), keywords, {
                each: (m) => {
                    m.style['-webkit-text-fill-color'] = fillColor;
                    m.style.opacity = .5;
                }
            });
            overlay.style.top = textarea.offsetTop + 'px';
            overlay.style.left = textarea.offsetLeft + 'px';
            overlay.style.width = textarea.offsetWidth + 'px';
            overlay.style.height = textarea.offsetHeight + 'px';
        };

        updOverlay();

        textarea.addEventListener('input', updOverlay);

        const observer = new MutationObserver(updOverlay);
        observer.observe(textarea, { attributes: true });

        window.addEventListener('resize', updOverlay);

        const cancelOverlay = () => {
            observer.disconnect();
            textarea.removeEventListener('input', updOverlay);
            window.removeEventListener('resize', updOverlay);
            overlay.remove();
        }
        return cancelOverlay;
    }
    // #endregion

    // #region 修复原文排版崩坏和<<>> fixOrigin(originElem)
    function fixOrigin(originElem) {
        originElem.innerHTML = originElem.innerHTML
        .replaceAll('<abbr title="noun.>" data-value=">">&gt;</abbr>', '&gt;')
        .replaceAll(/<var>(&lt;&lt;[^<]*?&gt;)<\/var>&gt;/g, '<var>$1&gt;</var>')
        .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">&gt;&gt;', '')
        .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;&gt;', '')
        .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;', '');
    }
    // #endregion

    // #region 还原上下文HTML源码 removeContextTags
    function removeContextTags() {
        const context = document.querySelector('.context');
        if (!context) return;
        context.innerHTML = context.innerHTML.replace(/<a.*?>(.*?)<\/a>/g, '$1').replace(/<(\/?)(li|b|i|u|h\d|span)>/g, '&lt;$1$2&gt;');
    }
    // #endregion

    // #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect
    const insertTag = debounce(async (tag) => {
        const textarea = document.querySelector('textarea.translation');
        const startPos = textarea.selectionStart;
        const endPos = textarea.selectionEnd;
        const currentText = textarea.value;

        const before = currentText.slice(0, startPos);
        const after = currentText.slice(endPos);

        await mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after.slice(0, -2)); // -2 去除\n

        textarea.selectionStart = startPos + 1;
        textarea.selectionEnd = endPos + 1;
    })

    let activeTag = null;
    let modifiedTags = [];
    const tagSelectController = new AbortController();
    const { tagSelectSignal } = tagSelectController;

    function tagSelectHandler(e) {
        if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
            activeTag &&= document.querySelector('.list-group-item.tag.active');
        }
        if (e.key === 'Enter') {
            if (!activeTag) return;
            if (!modifiedTags.includes(activeTag)) return;
            e.preventDefault();
            insertTag(activeTag?.textContent);
            activeTag = null;
        }
    }

    function updFixedTags() {

        const tags = document.querySelectorAll('.list-group-item.tag');
        activeTag = document.querySelector('.list-group-item.tag.active');
        modifiedTags = [];

        for (const tag of tags) {
            tag.innerHTML = tag.innerHTML.trim();
            if (tag.innerHTML.startsWith('&lt;&lt;') && !tag.innerHTML.endsWith('&gt;&gt;')) {
                tag.innerHTML += '&gt;';
                modifiedTags.push(tag);
            }
        }

        document.addEventListener('keyup', tagSelectHandler, { tagSelectSignal });

    }
    // #endregion

    // #region 将填充原文移到右边,增加填充原文并保存 tweakButtons
    function tweakButtons() {
        const copyButton = document.querySelector('button.btn-secondary:has(.fa-clone)');
        const rightButtons = document.querySelector('.right .btn-group');

        if (rightButtons) {
            if (copyButton) {
                rightButtons.insertBefore(copyButton, rightButtons.firstChild);
            }
            if (document.querySelector('#PZpaste')) return;
            const pasteSave = document.createElement('button');
            rightButtons.appendChild(pasteSave);
            pasteSave.id = 'PZpaste';
            pasteSave.type = 'button';
            pasteSave.classList.add('btn', 'btn-secondary');
            pasteSave.title = '填充原文并保存';
            pasteSave.innerHTML = '<i aria-hidden="true" class="far fa-save"></i>';
            pasteSave.addEventListener('click', async () => {
                await mockInput(document.querySelector('.editor-core .original')?.textContent);
                document.querySelector('.right .btn-primary')?.click();
            });
        }
    }
    // #endregion

    // #region 缩略对比差异中过长无差异文本 extractDiff
    function extractDiff() {
        document.querySelectorAll('.diff-wrapper:not(.PZedited)').forEach(wrapper => {
            wrapper.childNodes.forEach(node => {
                if (node.nodeType !== Node.TEXT_NODE || node.length < 200) return;

                const text = node.cloneNode();
                const expand = document.createElement('span');
                expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`;
                expand.style.cursor = 'pointer';
                expand.style.background = 'linear-gradient(to right, transparent, #aaf6, transparent)';
                expand.style.borderRadius = '2px';

                let time = 0;
                let isMoving = false;

                const start = () => {
                    time = Date.now()
                    isMoving = false;
                }
                const end = () => {
                    if (isMoving || Date.now() - time > 500) return;
                    expand.replaceWith(text);
                }

                expand.addEventListener('mousedown', start);
                expand.addEventListener('mouseup', end);
                expand.addEventListener('mouseleave', () => time = 0);

                expand.addEventListener('touchstart', start);
                expand.addEventListener('touchend', end);
                expand.addEventListener('touchcancel', () => time = 0);
                expand.addEventListener('touchmove', () => isMoving = true);

                node.replaceWith(expand);
            });
            wrapper.classList.add('PZedited');
        });
    }
    // #endregion

    // #region 点击对比差异绿色文字粘贴其中文本 initDiffClick
    function initDiffClick() {
        const addeds = document.querySelectorAll('.diff.added:not(.PZPedited)');
        for (const added of addeds) {
            added.classList.add('PZPedited');
            const text = added.textContent.replaceAll('\\n', '\n');
            added.style.cursor = 'pointer';
            added.addEventListener('click', () => {
                mockInsert(text);
            });
        }
    }
    // #endregion

    // #region 快速搜索原文 addCopySearchBtn
    async function addCopySearchBtn() {
        if (document.querySelector('#PZsch')) return;
        const originSch = document.querySelector('.btn-sm');
        if (!originSch) return;
        originSch.insertAdjacentHTML('beforebegin', '<button id="PZsch" type="button" class="btn btn-secondary btn-sm"><i aria-hidden="true" class="far fa-paste"></i></button>');
        const newSch = document.querySelector('#PZsch');
        newSch.addEventListener('click', async () => {
            const original = document.querySelector('.editor-core .original')?.textContent;
            let input = document.querySelector('.search-form input[type=search]');
            if (!input) {
                await (() => new Promise(resolve => resolve(originSch.click())))();
                input = document.querySelector('.search-form input[type=search]');
            }
            const submit = document.querySelector('.search-form button');
            await (() => new Promise(resolve => {
                input.value = original;
                input.dispatchEvent(new Event('input', {
                    bubbles: true,
                    cancelable: true,
                }));
                resolve();
            }))();
            submit.click();
        });
    }
    // #endregion

    // #region 进入下一条时关闭搜索结果 cancelSearchResult
    function cancelSearchResult() {
        const input = document.querySelector('.search-form input[type=search]');
        if (input) document.querySelectorAll('.btn-sm')[1]?.click();
    }
    // #endregion

    // #region 搜索结果对比差异 initSearchResultDiff(originTxt)
    function initSearchResultDiff(originTxt) {
        const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)');
        if (!strings[0]) return;

        const { $diff } = document.querySelector('main').__vue__;

        for (const string of strings) {
            const strHTML = string.innerHTML;
            const showDiff = document.createElement('a');
            showDiff.title = '查看差异';
            showDiff.href = '#';
            showDiff.target = '_self';
            showDiff.classList.add('small');
            showDiff.innerHTML = '<i aria-hidden="true" class="far fa-right-left-large"></i>';

            string.after(' ', showDiff);
            showDiff.addEventListener('click', function(e) {
                e.preventDefault();
                string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, originTxt);
                this.isShown = !this.isShown;
            });
        }
    }
    // #endregion

    // #region 高级搜索空格变+修复 fixAdvSch
    function fixAdvSch() {
        const inputs = document.querySelectorAll('#advancedSearch table input');
        if (!inputs[0]) return;
        const params = new URLSearchParams(location.search);
        const values = [...params.entries()].filter(([key, _]) => /(text|original|translation).?/.test(key)).map(([_, value]) => value.replaceAll(' ', '+'));
        for (const input of inputs) {
            if (values.includes(input.value)) {
                input.value = input.value.replaceAll('+', ' ');
                input.dispatchEvent(new Event('input', {
                    bubbles: true,
                    cancelable: true,
                }));
            }
        }
    }
    // #endregion

    // #region 自动保存全部相同词条 autoSaveAll
    const autoSave = localStorage.getItem('pzdiffautosave');
    function autoSaveAll() {
        const button = document.querySelector('.modal-dialog .btn-primary');
        if (autoSave && button.textContent === '保存全部') button.click();
    }
    // #endregion

    // #region 自动填充100%相似译文 autoFill100(suggests, originTxt)
    function autoFill100(suggests, originTxt) {
        if (!suggests[0]) return;
        const getSim = (suggest) => +suggest.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
        const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent);
        const getTranslation = (suggest) => suggest.querySelector('.translation').firstChild.textContent;
        for (const suggest of suggests) {
            const sim = getSim(suggest);
            const equalOrigin = [100, 101].includes(sim) || isEqualWithOneCharDifference(originTxt, getOriginal(suggest));
            if (equalOrigin) {
                mockInput(getTranslation(suggest));
                break;
            }
        }
    }

    function isEqualWithOneCharDifference(str1, str2) {
        if (str1 === str2) return true;

        if (Math.abs(str1.length - str2.length) > 1) return false;

        let differences = 0;
        const len1 = str1.length;
        const len2 = str2.length;
        let i = 0, j = 0;

        while (i < len1 && j < len2) {
            if (str1[i] !== str2[j]) {
                differences++;
                if (differences > 1) return false;

                if (len1 > len2) i++;
                else if (len2 > len1) j++;
                else {
                    i++;
                    j++;
                }
            } else {
                i++;
                j++;
            }
        }

        if (i < len1 || j < len2) differences++;

        return differences <= 1;
    }
    // #endregion

    // #region 重新排序历史词条 findTextWithin(suggests, originTxt) getDefaultSorted addReSortBtn
    function findTextWithin(suggests, originTxt) {
        if (!suggests[0]) return;
        originTxt = normalizeString(originTxt);
        const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent);
        for (const suggest of suggests) {
            if (getOriginal(suggest).includes(originTxt)) {
                suggest.parentNode.prepend(suggest);
                const header = suggest.querySelector('header');
                let headerSpan = header.querySelector('span');
                if (!headerSpan) {
                    headerSpan = document.createElement('span');
                    header.prepend(headerSpan);
                }
                if (headerSpan.textContent.includes('100%') || headerSpan.textContent.includes('101%')) break;
                headerSpan.textContent = '文本在中';
                break;
            }
        }
    }

    const reSortSuggests = (compareFn) => (suggests) => {
        if (!suggests[0]) return;
        const sorted = [...suggests].sort(compareFn);
        const parent = suggests[0].parentNode;
        const frag = document.createDocumentFragment();
        frag.append(...sorted);
        parent.innerHTML = '';
        parent.appendChild(frag);
    };

    const reSortSuggestsBySim = reSortSuggests((a, b) => {
        const getSim = (suggest) => {
            const simContainer = suggest.querySelector('header span');
            if (!simContainer) return 102; // 机器翻译参考
            const sim = +simContainer.textContent.split('\n')?.[2]?.trim().slice(0, -1);
            if (!sim) return 102; // 在文本中
            return sim;
        }
        return getSim(b) - getSim(a);
    });

    const reSortSuggestsByTime = reSortSuggests((a, b) => {
        const getTimestamp = (suggest) => {
            const time = suggest.querySelector('time')?.dateTime;
            if (!time) return Infinity;
            return +new Date(time);
        }
        return getTimestamp(b) - getTimestamp(a);
    });

    const reSortSuggestsByMem = (suggests) => {
        const sortType = localStorage.getItem('pzdiffsort') || 'sim';
        if (sortType === 'sim') {
            reSortSuggestsBySim(suggests);
        } else if (sortType === 'time') {
            reSortSuggestsByTime(suggests);
        }
    };

    let defaultSortedSuggests = [];
    const getDefaultSorted = (suggests) => {
        defaultSortedSuggests = suggests;
    };
    function recoverDefaultSort() {
        const parent = document.querySelector('.translation-memory .list');
        if (!parent) return;
        parent.append(...defaultSortedSuggests);
    }

    function addReSortBtn() {
        if (document.querySelector('.pzdiffsort')) return;
        const btn = document.createElement('a');
        btn.href = 'javascript:';
        btn.className = 'pzdiffsort';
        const icon = type => {
            const icon = document.createElement('i');
            icon.classList.add('far', `fa-${type}`);
            icon.ariaHidden = true;
            icon.style.cursor = 'pointer';
            return icon;
        }

        const simBtn = btn.cloneNode();
        simBtn.title = '按相似度排序';
        simBtn.append(icon('percentage'));
        simBtn.addEventListener('click', () => {
            const suggests = document.querySelectorAll('.string-item');
            reSortSuggestsByTime(suggests);
            localStorage.setItem('pzdiffsort', 'time');
            simBtn.replaceWith(timeBtn);
        });

        const timeBtn = btn.cloneNode();
        timeBtn.title = '按时间排序';
        timeBtn.append(icon('history'));
        timeBtn.addEventListener('click', () => {
            recoverDefaultSort();
            localStorage.setItem('pzdiffsort', 'default');
            timeBtn.replaceWith(defaultBtn);
        });

        const defaultBtn = btn.cloneNode();
        defaultBtn.title = '默认排序';
        defaultBtn.append(icon('sort-amount-down'));
        defaultBtn.addEventListener('click', () => {
            const suggests = document.querySelectorAll('.string-item');
            reSortSuggestsBySim(suggests);
            localStorage.setItem('pzdiffsort', 'sim');
            defaultBtn.replaceWith(simBtn);
        });

        const sortType = localStorage.getItem('pzdiffsort') || 'sim';
        const initBtn = {
            sim: simBtn,
            time: timeBtn,
            default: defaultBtn,
        };
        document.querySelector('.translation-memory .col-auto').after(initBtn[sortType]);
    }
    // #endregion

    // #region 初始化自动编辑 initAuto
    async function initAuto() {
        const avatars = await waitForElems('.nav-item.user-info');
        avatars.forEach(async (avatar) => {
            let harvesting = false;
            let translationPattern, skipPattern, userTime;
            avatar.insertAdjacentHTML('afterend', `<li class="nav-item"><a href="javascript:;" target="_self" class="PZpp nav-link" role="button">PP收割机</a></li>`);
            document.querySelectorAll('.PZpp').forEach(btn => btn.addEventListener('click', async (e) => {
                if (location.pathname.split('/')[3] !== 'strings') return;
                harvesting = !harvesting;
                if (harvesting) {
                    e.target.style.color = '#dc3545';
                    translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码:
    original(原文)
    oldTrans(现有译文)
    suggest(第1条翻译建议)
    suggestSim(上者匹配度,最大100)`, 'original');
                    if (translationPattern === null) return cancel();
                    skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码:
    original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签)
    oldTrans(现有译文)
    suggest(第1条翻译建议)
    suggestSim(上者匹配度,最大100)
    context(上下文内容)`, '');
                    if (skipPattern === null) return cancel();
                    if (skipPattern === '') skipPattern = 'false';
                    userTime = prompt('请确认生成译文后等待时间(单位:ms)', '500');
                    if (userTime === null) return cancel();
                    function cancel() {
                        harvesting = false;
                        e.target.style.color = '';
                    }
                } else {
                    e.target.style.color = '';
                    return;
                }

                const hideAlert = document.createElement('style');
                document.head.appendChild(hideAlert);
                hideAlert.innerHTML = '.alert-success.alert-global{display:none}';

                const checkboxs = [...document.querySelectorAll('.right .custom-checkbox')].slice(0, 2);
                const checkboxValues = checkboxs.map(e => e.__vue__.$data.localChecked);
                checkboxs.forEach(e => e.__vue__.$data.localChecked = true);

                const print = {
                    waiting: () => console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
                    skip: () => console.log('%cSKIP', 'background: #FFC107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
                    click: () => console.log('%cCLICK', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
                    end: () => console.log('%cTHE END', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
                }

                const INTERVAL = 100;
                let interval = INTERVAL;
                let lastInfo = null;

                function prepareWait() {
                    print.waiting();
                    interval = INTERVAL;
                    lastInfo = null;
                    return true;
                }

                function skipOrFin(originElem, nextButton) {
                    if (nextString(nextButton)) return false;
                    print.skip();
                    interval = 50;
                    lastInfo = [
                        originElem,
                        location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1
                    ];
                    return true;
                }

                function nextString(button) {
                    if (button.disabled) {
                        print.end();
                        harvesting = false;
                        e.target.style.color = '';
                        return true;
                    }
                    button.click();
                    return false;
                }

                try {
                    while (true) {
                        await sleep(interval);

                        if (lastInfo) {
                            const [ lastOrigin, lastPage ] = lastInfo;
                            // 已点击翻页,但原文未发生改变
                            const skipWaiting = (location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1) !== lastPage
                                             && document.querySelector('.editor-core .original') === lastOrigin;
                            if (skipWaiting && prepareWait()) continue;
                        }

                        const originElem = document.querySelector('.editor-core .original');
                        if (!originElem && prepareWait()) continue;
                        const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1];
                        if (!nextButton && prepareWait()) continue;

                        const original = originElem.textContent;
                        const oldTrans = document.querySelector('textarea.translation').value;
                        let suggest = null, suggestSim = 0;
                        if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) {
                            const suggestEle = (await waitForElems('.translation-memory .string-item .translation, .empty-sign'))[0];
                            if (suggestEle.classList.contains('empty-sign')) {
                                if (skipOrFin(originElem, nextButton)) continue; else break;
                            }
                            suggest = suggestEle.textContent;
                            suggestSim = +suggestEle.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
                            if ((translationPattern.includes('suggestSim') || skipPattern.includes('suggestSim')) && isNaN(suggestSim)) {
                                if (skipOrFin(originElem, nextButton)) continue; else break;
                            }
                        }
                        const context = document.querySelector('.context')?.textContent;

                        if (eval(skipPattern)) {
                            if (skipOrFin(originElem, nextButton)) continue; else break;
                        }

                        const translation = eval(translationPattern);
                        if (!translation && prepareWait()) continue;

                        await mockInput(translation);
                        await sleep(userTime);
                        if (!harvesting) break; // 放在等待后,以便在等待间隔点击取消

                        const translateButton = document.querySelector('.right .btn-primary');
                        if (!translateButton) {
                            if (skipOrFin(originElem, nextButton)) continue; else break;
                        } else {
                            translateButton.click();
                            print.click();
                            interval = INTERVAL;
                            lastInfo = null;
                            continue;
                        }
                    }
                } catch (e) {
                    console.error(e);
                    alert('出错了!');
                } finally {
                    hideAlert.remove();
                    checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] });
                }

            }));
        });
    }
    // #endregion

    // #endregion

    // #region 函数调用逻辑

    addHotkeys();
    initAuto();

    let stringPageTurned = true;
    async function actByPath(path) {
        if (path.split('/').pop() === 'strings') {

            let original;
            let lastOriginHTML = '';
            let toObserve = document.body;

            const observer = new MutationObserver((mutations) => {

                fixAdvSch();
                if (shouldSkip()) return;

                original = document.querySelector('.editor-core .original');
                if (!original) return;
                const originUpded = original.innerHTML !== lastOriginHTML;
                lastOriginHTML = original.innerHTML;

                observer.disconnect();
                initDiffClick();
                extractDiff();

                const markAll = () => {
                    fixOrigin(original);
                    removeContextTags();
                    markSearchParams();
                    markContext(original.textContent);
                };

                if (stringPageTurned) {
                    if (!originUpded) {
                        connectObserve();
                        return;
                    }
                    console.debug('framework loaded');
                    initDropMark();
                    initMarkJS();
                    tweakButtons();
                    addCopySearchBtn();
                    addReSortBtn();
                    watchContextBtn();
                    markAll();
                    stringPageTurned = false;
                    connectObserve();
                    return;
                }

                if (originUpded) {
                    console.debug('origin upded');
                    cancelSearchResult();
                    markAll();
                }

                for (const mutation of mutations) {
                    const { addedNodes, removedNodes } = mutation;
                    // console.debug({ addedNodes, removedNodes });
                    if (addedNodes.length === 1) {
                        const node = addedNodes[0];
                        if (node.matches?.('.list-group.tags')) {
                            updFixedTags();
                            continue;
                        }
                        if (node.matches?.('.string-item a.small')) {
                            node.remove();
                            continue;
                        }
                        if (node.matches?.('.modal-backdrop')) {
                            autoSaveAll();
                            continue;
                        }
                    } else if (removedNodes.length === 1) {
                        const node = removedNodes[0];
                        if (mutation.target.classList?.contains('translation-memory')
                         && node.classList?.contains('loading')) {
                            console.debug('suggests loaded');
                            const suggests = document.querySelectorAll('.string-item');
                            findTextWithin(suggests, original.textContent);
                            getDefaultSorted(suggests);
                            initSearchResultDiff(original.textContent);
                            autoFill100(suggests, original.textContent);
                            reSortSuggestsByMem(suggests);
                            continue;
                        }
                        if (node.matches?.('.list-group.tags')) tagSelectController.abort();
                    }
                }

                connectObserve();
            });

            connectObserve();
            function connectObserve() {
                observer.observe(toObserve, {
                    childList: true,
                    subtree: true,
                });
            }

            return observer;

        } else if (path.split('/').at(-2) === 'issues') {
            waitForElems('.text-content p img').then((imgs) => imgs.forEach(mediumZoom));
        } else if (path.split('/').pop() === 'history') {

            let observer = new MutationObserver(() => {
                observer.disconnect();
                extractDiff();
                connectObserve();
            });

            connectObserve();
            function connectObserve() {
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                });
            }

            return observer;

        }
    }
    let cancelAct = await actByPath(location.pathname);
    (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async (to, from) => {
        dropLastTextareaMark?.();
        if (JSON.stringify(to.query) !== JSON.stringify(from.query)) {
            console.debug('query changed');
            if (to.path.split('/').pop() === 'strings') {
                stringPageTurned = true;
            }
        }
        if (to.path === from.path) return;
        tagSelectController.abort();
        cancelAct?.disconnect();
        console.debug('path changed');
        cancelAct = await actByPath(to.path);
    });

    // #endregion

    // #region 通用工具函数
    function waitForElems(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelectorAll(selector));
            }

            const observer = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    resolve(document.querySelectorAll(selector));
                    observer.disconnect();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function sleep(delay) {
        return new Promise((resolve) => setTimeout(resolve, delay));
    }

    function mockInput(text) {
        return new Promise((resolve) => {
            const textarea = document.querySelector('textarea.translation');
            if (!textarea) return;
            textarea.value = text;
            textarea.dispatchEvent(new Event('input', {
                bubbles: true,
                cancelable: true,
            }));
            return resolve(0);
        })
    }

    function mockInsert(text) {
        const textarea = document.querySelector('textarea.translation');
        if (!textarea) return;
        const startPos = textarea.selectionStart;
        const endPos = textarea.selectionEnd;
        const currentText = textarea.value;

        const before = currentText.slice(0, startPos);
        const after = currentText.slice(endPos);

        mockInput(before + text + after);

        textarea.selectionStart = startPos + text.length;
        textarea.selectionEnd = endPos + text.length;
    }

    function debounce(func, timeout = 300) {
        let called = false;
        return (...args) => {
            if (!called) {
                func.apply(this, args);
                called = true;
                setTimeout(() => called = false, timeout);
            }
        };
    }

    function normalizeString(str) {
        if (!str) return '';
        return str
          .replace(/[,.;'"-]/g, '')
          .replace(/\s+/g, '')
          .toLowerCase();
    }
    // #endregion

})();