您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ParaTranz enhanced
当前为
// ==UserScript== // @name ParaTranz diff // @namespace https://paratranz.cn/users/44232 // @version 0.7.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 function initSkip() { waitForElems('.string-list .empty-sign').then(() => { if (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 = () => console.log('PZdiff: no search'); 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 = () => { markNorm('.editor-core .original', text); return markEditing(text); } } else if (original) { markSearchParams = () => { markNorm('.editor-core .original', original); } } else if (translation) { markSearchParams = () => { return markEditing(translation); } } else if (context) { markSearchParams = () => { markNorm('.context', context); } } else { markSearchParams = () => console.log('PZdiff: no search'); } } 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('&', '&').replaceAll('<', '<').replaceAll('>', '>').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, '<$1$2>'); originTxt = originTxt.replaceAll('<br>', '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); 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=">">></abbr>', '>') .replaceAll(/<var>(<<[^<]*?>)<\/var>>/g, '<var class="PZvar">$1></var>') .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">>>', '') .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>>>', '') .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>>', ''); } // #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; }) function fixTagSelect() { 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('<<') && !tag.innerHTML.endsWith('>>')) { tag.innerHTML += '>'; 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') { event.preventDefault(); if (!activeTag) return; if (!modifiedTags.includes(activeTag)) return; insertTag(activeTag?.textContent); document.removeEventListener('keyup', handler); } } } // #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, #dcc8ff, transparent)'; expand.style.borderRadius = '2px'; let time = 0; const start = () => time = Date.now(); const end = () => { if (Date.now() - time > 500) return; expand.after(text); expand.remove(); } 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); node.after(expand); node.remove(); }); 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 初始化自动编辑 initAuto function initAuto() { waitForElems('.nav-item.user-info').then((bannerL) => { const banner = bannerL[0]; let harvesting = false; let translationPattern, skipPattern, interval; banner.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] }); }); }); } // #endregion // #endregion addHotkeys(); initAuto(); let lastPath = location.pathname; function actByPath() { lastPath = location.pathname; if (location.pathname.split('/').pop() === 'strings') { initSkip(); let original; let observer = new MutationObserver(() => { original = document.querySelector('.editor-core .original'); if (!original) return; observer.disconnect(); markContext(original.textContent); fixOrigin(original); tweakButtons(); fixTagSelect(); markSearchParams(); clickDiff(); extractDiff(); observer.observe(document.getElementsByTagName('body')[0], { childList: true, subtree: true, }); }); observer.observe(document.getElementsByTagName('body')[0], { childList: true, subtree: true, }); } 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.getElementsByTagName('body')[0], { childList: true, subtree: true, }); }); observer.observe(document.getElementsByTagName('body')[0], { childList: true, subtree: true, }); } } actByPath(); document.querySelector('main').__vue__.$router.afterHooks.push(()=>{ dropLastMark?.(); dropLastMark = updMark(); if (lastPath === location.pathname) return; 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 })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址