ParaTranz diff

ParaTranz enhanced

目前為 2025-01-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ParaTranz diff
// @namespace    https://paratranz.cn/users/44232
// @version      0.9.0
// @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
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    // #region 主要功能

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

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

    // #region 更多搜索高亮 markSearchParams
    let markSearchParams = () => {};
    function updMark() {
        const params = new URLSearchParams(location.search);
        const text = params.get('text');
        const original = params.get('original');
        const translation = params.get('translation');
        const context = params.get('context');

        if (text) {
            markSearchParams = (isOriginUpd) => {
                if (isOriginUpd) markNorm('.editor-core .original', text);
                return markEditing(text);
            }
        } else if (original) {
            markSearchParams = (isOriginUpd) => {
                if (isOriginUpd) markNorm('.editor-core .original', original);
            }
        } else if (translation) {
            markSearchParams = () => {
                return markEditing(translation);
            }
        } else if (context) {
            markSearchParams = () => {
                markNorm('.context', context);
            }
        } else {
            markSearchParams = () => {};
        }
    }
    let dropLastMark = updMark();

    function markNorm(selector, toMark) {
        const container = document.querySelector(selector);
        if (!container) return;

        let toMarkPattern = toMark;
        if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
            toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
        }

        const HTML = container.innerHTML;
        const currentMark = `<mark class="PZS">${toMark}</mark>`;
        if (HTML.includes(currentMark)) return;
        container.innerHTML = HTML.replaceAll('<mark class="PZS">', '').replace(/(?<=>|^)([^<]*?)(?=<|$)/g, (match) => {
            if (typeof toMarkPattern === 'string') {
                return match.replaceAll(toMarkPattern, currentMark);
            } else {
                return match.replace(toMarkPattern, '<mark class="PZS">$1</mark>');
            }
        });
    }

    function markEditing(toMark) {
        const 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['overflow-y'] = 'hidden';
        overlay.style.resize = 'none';

        textarea.parentNode.appendChild(overlay);

        const updOverlay = () => {
            let toMarkPattern = toMark.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\\n', '<br>');
            if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
                toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
            }
            overlay.innerText = textarea.value;
            if (typeof toMarkPattern === 'string') {
                overlay.innerHTML = overlay.innerHTML.replaceAll(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
                    window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
                };opacity:.5">${toMarkPattern}</mark>`);
            } else {
                overlay.innerHTML = overlay.innerHTML.replace(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
                    window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
                };opacity:.5">$1</mark>`);
            }
            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, childList: true, subtree: true });

        window.addEventListener('resize', updOverlay);

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

    // #region 高亮上下文 markContext(originTxt)
    function markContext(originTxt) {
        const contextBox = document.querySelector('.context');
        if (!contextBox) return;

        const context = contextBox.innerHTML.replaceAll(/<a.*?>(.*?)<\/a>/g, '$1').replaceAll(/<(\/?)(li|b|u|h\d|span)>/g, '&lt;$1$2&gt;');
        originTxt = originTxt.replaceAll('<br>', '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
        if (contextBox.querySelector('#PZmark')?.textContent === originTxt) return;
        contextBox.innerHTML = context.replace('<mark id="PZmark" class="mark">', '').replace(originTxt, `<mark id="PZmark" class="mark">${originTxt}</mark>`);
    }
    // #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 class="PZvar">$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 修复 Ctrl 唤起菜单的<<>> fixTagSelect
    const insertTag = debounce((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);

        mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after);

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

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

    function tagSelectHandler(event) {
        if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
            activeTag &&= document.querySelector('.list-group-item.tag.active');
        }
        if (event.key === 'Enter') {
            if (!activeTag) return;
            if (!modifiedTags.includes(activeTag)) return;
            event.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 点击对比差异绿色文字粘贴其中文本 clickDiff
    function clickDiff() {
        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 快速搜索原文 copySearch
    async function copySearch() {
        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-clone"></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.mt-3 input[type=search]');
            if (!input) {
                await (() => new Promise(resolve => resolve(originSch.click())))();
                input = document.querySelector('.search-form.mt-3 input[type=search]');
            }
            input.value = original;
            input.dispatchEvent(new Event('input', {
                bubbles: true,
                cancelable: true,
            }));
        });
    }
    // #endregion

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

        const original = document.querySelector('.editor-core .original')?.textContent;
        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() {
                string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, original);
                this.isShown = !this.isShown;
            })
        }
    }

    // #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,
                }));
            }
        }
    }

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

    // #region 自动填充100%相似译文 autoFill100
    function autoFill100() {
        const suggests = document.querySelectorAll('.string-item');
        const getSim = (suggest) => +suggest.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
        const getTranslation = (suggest) => suggest.querySelector('.translation').textContent;
        for (const suggest of suggests) {
            if ([100, 101].includes(getSim(suggest))) {
                mockInput(getTranslation(suggest));
                break;
            }
        }
    }

    // #region 重新排序历史词条 reSortSuggestions
    function reSortSuggestions() {
        const suggests = document.querySelectorAll('.string-item');
        const getSim = (suggest) => +suggest?.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
        if (!getSim(suggests[0])) return;
        const sorted = [...suggests].sort((a, b) => getSim(b) - getSim(a));
        const parent = suggests[0].parentNode;
        const frag = document.createDocumentFragment();
        frag.append(...sorted);
        parent.innerHTML = '';
        parent.appendChild(frag);
    }

    // #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]
                    ];
                    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] !== 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')) {
                            suggest = (await waitForElems('.translation-memory .translation, .empty-sign'))[0].textContent;
                            suggestSim = +(await waitForElems('.translation-memory header span span'))[0].textContent.split('\n')?.[2].trim().slice(0, -1);
                        }
                        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

    addHotkeys();
    initAuto();

    let lastPath = location.pathname;
    async function actByPath() {
        lastPath = location.pathname;
        if (location.pathname.split('/').pop() === 'strings') {

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

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

                fixAdvSch();

                original = document.querySelector('.editor-core .original');
                if (!original) return;
                const isOriginUpd = lastOriginText && original.textContent !== lastOriginText;
                lastOriginText = original.textContent;

                observer.disconnect();
                initSkip();
                markContext(original.textContent);
                markSearchParams(isOriginUpd);
                fixOrigin(original);
                tweakButtons();
                clickDiff();
                extractDiff();
                copySearch();

                if (isOriginUpd) {
                    const input = document.querySelector('.search-form.mt-3 input[type=search]');
                    if (input) document.querySelectorAll('.btn-sm')[1]?.click();
                }

                for (const mutation of mutations) {
                    const { addedNodes, removedNodes } = mutation;
                    for (const node of addedNodes) {
                        console.debug('added', node);
                        if (node.matches?.('.list-group.tags')) updFixedTags();
                        if (node.matches?.('.string-item a.small')) node.remove();
                        if (node.matches?.('.modal-backdrop')) autoSaveAll();
                    }
                    for (const node of removedNodes) {
                        console.debug('removed ', node);
                        if (node.matches?.('.loading')) { // 历史加载完成
                            searchDiff();
                            autoFill100();
                            reSortSuggestions();
                        }
                        if (node.matches?.('.list-group.tags')) tagSelectController.abort();
                    }
                }

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

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

            return observer;

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

                observer.disconnect();
                extractDiff();

                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                });
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
            return observer;
        }
    }
    let cancelAct = await actByPath();
    (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async ()=>{
        dropLastMark?.();
        dropLastMark = updMark();
        if (lastPath === location.pathname) return;
        cancelAct?.disconnect();
        console.debug('path changed');
        cancelAct = await actByPath();
    });

    // #region utils
    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);
          }
        };
    }
    // #endregion

})();