ParaTranz enhanced
当前为
// ==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('&', '&').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';
};
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, '<$1$2>');
original = original.replaceAll('<br>', '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
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=">">></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>>', '');
}
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('<<') && !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') {
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);
}
};
}
})();