ParaTranz diff

ParaTranz enhanced

当前为 2024-09-04 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(async function() {
    'use strict';

    let psearch = () => console.log('PZdiff: no search');
    function genPsearch() {
        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) {
            psearch = () => {
                PZSmark('.editor-core .original', text);
                PZStextarea(text);
            }
        } else if (original) {
            psearch = () => {
                PZSmark('.editor-core .original', original);
            }
        } else if (translation) {
            psearch = () => {
                PZStextarea(translation);
            }
        } else if (context) {
            psearch = () => {
                PZSmark('.context', context);
            }
        }
    }
    genPsearch();
    document.querySelector('main').__vue__.$router.afterHooks.push(()=>{
        genPsearch();
    });

    function PZSmark(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 PZStextarea(toMark) {
        // 感谢Copilot
        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 updateOverlay = () => {
            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';
        };

        updateOverlay();

        textarea.addEventListener('input', updateOverlay);

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

        window.addEventListener('resize', updateOverlay);
    }

    waitForElm('.nav-item.user-info').then((elm) => {
        let harvesting = false;
        let translationPattern, skipPattern, interval;
        elm.insertAdjacentHTML('afterend', `<li class="nav-item"><a id="PZpp" href="javascript:;" target="_self" class="nav-link" role="button">PP收割机</a></li>`);
        document.querySelector('#PZpp').addEventListener('click', async (e) => {
            if (location.pathname.split('/')[3] !== 'strings') return;
            harvesting = !harvesting;
            if (harvesting) {
                e.target.style.color = '#dc3545';
                translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码:
original(原文)
document.querySelector('textarea.translation')?.value(现有译文)
document.querySelectorAll('.translation-memory .translation')?.[0].textContent(第1条翻译建议)`, 'original');
                if (translationPattern === null) return cancel();
                skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码:
original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签)
document.querySelector('textarea.translation')?.value(现有译文)
document.querySelector('.context').textContent(上下文内容)`, '');
                if (skipPattern === null) return cancel();
                if (skipPattern === '') skipPattern = 'false';
                interval = prompt('请确认每次操作时间间隔(单位:ms)', '100');
                if (interval === null) return cancel();
                function cancel() {
                    harvesting = false;
                    e.target.style.color = '';
                }
            } else {
                e.target.style.color = '';
                return 0;
            }

            const hideAlert = document.createElement('style');
            document.head.appendChild(hideAlert);
            hideAlert.innerHTML = '.alert-success.alert-global{display:none}';
            const checkboxs = Array.from(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);

            await (function harvest(time, skipInfo) {
                return new Promise(async (resolve) => {
                    await sleep(time);
                    if (!harvesting) return resolve(0);
                    if (skipInfo) {
                        const skipWaiting = location.search.match(/(?<=(\?|&)page=)\d+/g) !== skipInfo[1]
                                   && document.querySelector('.editor-core .original') === skipInfo[0];
                        if (skipWaiting) {
                            return resolve(harvest(time, skipInfo));
                        }
                    }
                    const original = document.querySelector('.editor-core .original')?.textContent;
                    const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1];
                    if (!original || !nextButton) {
                        console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                        return resolve(harvest(interval));
                    }

                    const translation = eval(translationPattern);
                    if (!translation) {
                        console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                        return resolve(harvest(interval));
                    }
                    if (eval(skipPattern)) {
                        console.log('%cSKIP!', 'background: #ffc107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                        if (nextString()) return resolve(0);
                        return resolve(harvest(interval/2, [
                            document.querySelector('.editor-core .original'),
                            location.search.match(/(?<=(\?|&)page=)\d+/g)
                        ]));
                    }

                    await mockInput(translation);
                    const translateButton = document.querySelector('.right .btn-primary');
                    if (!translateButton) {
                        if (nextButton) {
                            console.log('%cSKIP!', 'background: #ffc107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                            if (nextString()) return resolve(0);
                            console.log(original)
                            return resolve(harvest(interval/2, [
                                document.querySelector('.editor-core .original'),
                                location.search.match(/(?<=(\?|&)page=)\d+/g)
                            ]));
                        }
                    } else {
                        console.log('%cCLICK!', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                        translateButton.click();
                        return resolve(harvest(interval));
                    }

                    function nextString() {
                        if (nextButton.disabled) {
                            console.log('%cTHE END!', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px');
                            harvesting = false;
                            e.target.style.color = '';
                            return true;
                        }
                        nextButton.click();
                        return false;
                    }
                });
            })(interval);

            hideAlert.remove();
            checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] });

        });
    })

        //if (location.pathname.split('/')[3] === 'strings') {

            let original;
            waitForElm('.string-list .empty-sign').then(() => {
                    if (location.search.match(/(\?|&)page=\d+/g)) {
                        document.querySelector('.pagination .page-item a')?.click();
                    }
                });

            const previous = debounce(() => document.querySelectorAll('.navigation .btn-secondary')?.[0]?.click());
            const next = debounce(() => document.querySelectorAll('.navigation .btn-secondary')?.[1]?.click());
            (function photkey() {
                document.addEventListener('keydown', (event) => {
                    if (event.ctrlKey && event.shiftKey && event.key === 'V') {
                        event.preventDefault();
                        mockInput(document.querySelector('.editor-core .original')?.textContent);
                    }
                    if (event.ctrlKey && event.altKey) {
                        if (event.key === 'ArrowLeft') {
                            previous();
                        } else if (event.key === 'ArrowRight') {
                            next();
                        }
                    }
                });
            })();

            const modifyTag = 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 observer = new MutationObserver(() => {

                original = document.querySelector('.editor-core .original');
                if (!original) return;

                observer.disconnect();
                pdiff(original.textContent);
                pcontext(original.textContent);
                pvar(original);
                pbutton();
                ptagselect();
                psearch();

                observer.observe(document.getElementsByTagName('body')[0], {
                    childList: true,
                    subtree: true,
                });
            });

            observer.observe(document.getElementsByTagName('body')[0], {
                childList: true,
                subtree: true,
            });

            // waitForElm('#spc').then((elm) => {
            //     elm.remove();
            // });

            function pdiff(original) {
                const oldBoxs = document.querySelectorAll('.translation-memory .string-item');

                if (!oldBoxs[0] || document.querySelector('.PZdiff')) return;

                for (const oldBox of oldBoxs) {
                    if (!oldBox.querySelector('.original')) continue;

                    const one = oldBox.querySelector('.original').textContent.replaceAll('\\n', '\n'),
                          other = original;

                    if (!one || !other) return;

                    let span = null;

                    const diff = Diff.diffWords(one, other),
                          PZdiff = document.createElement('div');

                    diff.forEach((part) => {
                      const color = part.added ? '#28a745' :
                                    part.removed ? '#dc3545' : 'grey';
                      span = document.createElement('span');
                      span.style.color = color;
                      span.appendChild(document
                        .createTextNode(part.value));
                      PZdiff.appendChild(span);
                    });

                    oldBox.querySelector('.original').appendChild(PZdiff);
                    PZdiff.classList.add('PZdiff');
                }
            }

            function pcontext(original) {
                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;');
                original = original.replaceAll('<br>', '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
                if (contextBox.querySelector('#PZmark')?.textContent === original) return;
                contextBox.innerHTML = context.replace('<mark id="PZmark" class="mark">', '').replace(original, `<mark id="PZmark" class="mark">${original}</mark>`);
            }

            function pvar(original) {
                original.innerHTML  = original.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;', '');
            }

            function pbutton() {
                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();
                    });
                }
            }

            function ptagselect() {
                const tags = document.querySelectorAll('.list-group-item.tag');
                let activeTag;
                const modifiedTags = [];
                if (tags[0]) {
                    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);
                        }
                    }
                    activeTag = document.querySelector('.list-group-item.tag.active');
                    document.addEventListener('keyup', handler);
                }

                function handler(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;
                        modifyTag(activeTag?.textContent);
                        document.removeEventListener('keyup', handler);
                    }
                }
            }
        //}

        //if (location.pathname.split('/')[3] === 'issues' && location.pathname.split('/')[4]) {
            waitForElm('.text-content p img').then((elm) => {
                mediumZoom(elm);
            });
        //}


    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    resolve(document.querySelector(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 debounce(func, timeout = 300) {
        let called = false;
        return (...args) => {
          if (!called) {
            func.apply(this, args);
            called = true;
            setTimeout(() => {
              called = false;
            }, timeout);
          }
        };
    }

})();