您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ParaTranz enhanced
当前为
// ==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('&', '&').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; }) 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('<<') && !tag.innerHTML.endsWith('>>')) { tag.innerHTML += '>'; 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 })();