// ==UserScript==
// @name ParaTranz Enhanced
// @namespace https://paratranz.cn/users/44232
// @version 0.12.0
// @description 添加了很多小功能,希望以后官方能添加
// @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
// @require https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
/* global Mark mediumZoom GM_addStyle */
(async function() {
'use strict';
GM_addStyle(/* css */`
.strings .string :is(.original, .translation) {
text-overflow: ellipsis ellipsis !important;
}
#PZSoverlay {
position: absolute !important;
pointer-events: none !important;
background: transparent !important;
-webkit-text-fill-color: transparent !important;
overflow-y: hidden !important;
resize: none !important;
}
#PZSoverlay mark {
mix-blend-mode: multiply;
}
.pz-expandable {
cursor: pointer;
background: linear-gradient(to right, transparent, #aaf6, transparent);
borderRadius: 2px;
}
.add-clickable {
cursor: pointer;
}
`);
// #region 主要功能函数
// #region 自动跳过空白页 shouldSkip
function shouldSkip() {
if (document.querySelector('.string-list .empty-sign') &&
location.search.match(/(\?|&)page=\d+/g)) {
document.querySelector('.pagination button')?.click();
return true;
}
}
// #endregion
// #region 添加快捷键 addHotkeys
function addHotkeys() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'V') {
e.preventDefault();
mockInput(document.querySelector('.editor-core .original')?.textContent);
}
});
}
// #endregion
// #region 更多搜索高亮 initDropMark markSearchParams initMarkJS watchContextBtn
let markSearchParams = () => {}, markSearchParamsInList = () => {};
const mergeObjects = (obj1, obj2) => {
const merged = {};
for (const key of Object.keys(obj1)) {
merged[key] = [...obj1[key], ...obj2[key]];
}
return merged;
};
function updMark() {
const params = new URLSearchParams(location.search);
const getParams = (type) => {
return {
contains: [...params.getAll(type)],
startsWith: [...params.getAll(`${type}^`)],
endsWith: [...params.getAll(`${type}$`)],
match: [...params.getAll(`${type}~`)]
};
};
const texts = getParams('text');
const originals = getParams('original');
const translations = getParams('translation');
const contexts = getParams('context');
const originKeywords = mergeObjects(texts, originals);
const translationKeywords = mergeObjects(texts, translations);
const contextKeywords = contexts;
markSearchParams = () => {
markOrigin(originKeywords);
markContext(contextKeywords);
};
markSearchParamsInList = () => {
markListOriginal(originKeywords);
markListTranslation(translationKeywords);
};
if (Object.values(translationKeywords).filter(v => v).length) {
const dropMark = markEditing(translationKeywords);
return dropMark;
}
}
let dropLastTextareaMark;
const initDropMark = () => dropLastTextareaMark = updMark();
let originMark, contextMark, listOriginalMark, listTranslationMark;
function initMarkJS() {
const original = document.querySelector('.editor-core .original');
originMark = new Mark(original);
original.addEventListener('click', (e) => {
if (e.target.tagName === 'MARK') {
const originalElement = e.target.parentElement;
originalElement.click();
}
});
const context = document.querySelector('.context');
if (context) contextMark = new Mark(context);
const getUnmark = m => document.querySelectorAll(`.string-list .${m}:not(:has( mark:not([data-markjs])))`);
listOriginalMark = new Mark(getUnmark('original'));
listTranslationMark = new Mark(getUnmark('translation'));
markSearchParamsInList();
ensureListMarkInView();
}
function watchContextBtn() {
const btn = document.querySelector('.float-right a');
if (!btn) return;
btn.addEventListener('click', () => {
const context = document.querySelector('.context');
if (!context) return;
removeContextTags();
const original = document.querySelector('.editor-core .original').textContent;
contextMark = new Mark(context);
markContext(original);
});
}
function ensureListMarkInView() {
document.querySelectorAll(`.string-list :is(.original, .translation) mark:first-of-type`).forEach(mark => {
const container = mark.parentElement;
const containerRect = container.getBoundingClientRect();
const markRect = mark.getBoundingClientRect();
const markLeft = markRect.left - containerRect.left;
const containerWidth = container.clientWidth;
const containerCenter = containerWidth / 2;
if (markLeft >= 0 && markLeft <= containerCenter) return;
const targetScrollLeft = markLeft - containerCenter + (markRect.width / 2);
container.scrollLeft = targetScrollLeft;
});
}
function mark(target, keywords, options, caseSensitive) {
if (!target) return;
target.unmark();
caseSensitive ??= !document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked;
const flags = caseSensitive ? 'g' : 'ig';
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let patterns;
if (typeof keywords === 'string') {
patterns = escapeRegExp(keywords);
} else {
const { contains, startsWith, endsWith, match } = keywords;
patterns = [
...contains.map(keyword => `(${escapeRegExp(keyword)})`),
...startsWith.map(keyword => `^(${escapeRegExp(keyword)})`),
...endsWith.map(keyword => `(${escapeRegExp(keyword)})$`),
match
].filter(p => p.length).join('|');
}
if (patterns) {
const regex = new RegExp(patterns, flags);
target.markRegExp(regex, {
acrossElements: true,
separateWordSearch: false,
...options
});
}
}
function markOrigin(keywords) {
mark(originMark, keywords);
}
function markList(markObject, keywords) {
mark(markObject, keywords, { acrossElements: false });
}
function markListOriginal(keywords) {
markList(listOriginalMark, keywords);
}
function markListTranslation(keywords) {
markList(listTranslationMark, keywords);
}
function markContext(originTxt) {
mark(contextMark, originTxt, { className: 'mark'});
}
function markEditing(keywords) {
let 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);
}
textarea.parentNode.appendChild(overlay);
const updOverlay = () => {
overlay.innerText = textarea.value;
mark(new Mark(overlay), keywords);
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 });
const resizeObserver = new ResizeObserver(updOverlay);
resizeObserver.observe(textarea);
const cancelOverlay = () => {
observer.disconnect();
textarea.removeEventListener('input', updOverlay);
resizeObserver.disconnect();
overlay.remove();
}
return cancelOverlay;
}
// #endregion
// #region 修复原文排版崩坏和<<>> fixOrigin(originElem)
// function fixOriginTwins(originElem) {
// fixOriginTerms(originElem);
// originElem.innerHTML = originElem.innerHTML
// .replaceAll(/<var data-type="(\d)">(<<[^<]*?>)<\/var>>/g, '<var data-type="$1">$2></var>');
// }
// function fixOriginTerms(originElem) {
// originElem.innerHTML = originElem.innerHTML
// .replaceAll('<abbr title="noun.>" data-value=">">></abbr>', '>')
// .replaceAll('<abbr="" title="noun.>" data-value=">">>\n>', '>\n</var>')
// .replaceAll('<var class="<abbr" title="noun.“”" data-value="“”">"line-break<abbr title="noun.“”" data-value="“”">"</abbr>>\n</var>', '<var class="line-break">\n</var>')
// .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 还原上下文HTML源码 removeContextTags
function removeContextTags() {
const context = document.querySelector('.context .well');
if (!context) return;
// eslint-disable-next-line no-self-assign
context.textContent = context.textContent;
}
// #endregion
// #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect
// const insertTag = debounce(async (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);
// await mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after.slice(0, -2)); // -2 去除\n
// textarea.selectionStart = startPos + 1;
// textarea.selectionEnd = endPos + 1;
// })
// let activeTag = null;
// let modifiedTags = [];
// const tagSelectController = new AbortController();
// const { tagSelectSignal } = tagSelectController;
// function tagSelectHandler(e) {
// if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
// activeTag &&= document.querySelector('.list-group-item.tag.active');
// }
// if (e.key === 'Enter') {
// if (!activeTag) return;
// if (!modifiedTags.includes(activeTag)) return;
// e.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 extractText
function extractDiff() {
document.querySelectorAll('.diff-wrapper:not(.extracted-diff)').forEach(wrapper => {
[...wrapper.childNodes].filter(node => node.length >= 200).forEach(node => extractText(node));
wrapper.classList.add('extracted-diff');
});
}
function extractHist() {
extractDiff();
const nonDiff = [...document.querySelectorAll(':is(.original, .translation):not(.extracted-diff)')];
nonDiff.forEach(node => {
if (node.textContent.length >= 200) {
// eslint-disable-next-line no-self-assign
node.textContent = node.textContent;
extractText(node.firstChild, true);
}
node.classList.add('extracted-diff');
});
}
function extractText(node, dual = false) {
const text = node.cloneNode();
const expand = document.createElement('span');
expand.classList.add('pz-expandable');
expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`;
let time = 0;
let isMoving = false;
expand.expandEvent = () => expand.replaceWith(text);
const start = () => {
time = Date.now()
isMoving = false;
}
const end = () => {
if (isMoving || Date.now() - time > 500) return;
if (dual) {
const wrapper = expand.parentElement;
const counter = wrapper.previousElementSibling || wrapper.nextElementSibling;
counter.firstElementChild?.expandEvent?.();
}
expand.expandEvent();
}
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);
}
// #endregion
// #region 点击对比差异绿色文字粘贴其中文本 initDiffClick
function initDiffClick() {
const addeds = document.querySelectorAll('.diff.added:not(.add-clickable)');
for (const added of addeds) {
added.classList.add('add-clickable');
const text = added.textContent.replaceAll('\\n', '\n').replaceAll('↵', '\n');
added.addEventListener('click', () => {
mockInsert(text);
});
}
}
// #endregion
// #region 快速搜索原文 addCopySearchBtn
async function addCopySearchBtn() {
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-paste"></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 input[type=search]');
if (!input) {
await (() => new Promise(resolve => resolve(originSch.click())))();
input = document.querySelector('.search-form input[type=search]');
}
const submit = document.querySelector('.search-form button');
await (() => new Promise(resolve => {
input.value = original;
input.dispatchEvent(new Event('input', {
bubbles: true,
cancelable: true,
}));
resolve();
}))();
submit.click();
});
}
// #endregion
// #region 进入下一条时关闭搜索结果 cancelSearchResult
function cancelSearchResult() {
const input = document.querySelector('.search-form input[type=search]');
if (input) document.querySelectorAll('.btn-sm')[1]?.click();
}
// #endregion
// #region 页内搜索结果高亮 renderSearchResult(originTxt)
let inPageKeyword = '';
let resultMark;
function renderSearchResult() {
const results = document.querySelectorAll('.string-item :is(.original, .translation)');
resultMark = new Mark(results);
mark(resultMark, inPageKeyword, {
separateWordSearch: true,
ignorePunctuation: ":;.,-–—‒_(){}[]!'\"+=".split(""),
}, false);
}
// #endregion
// #region 建议对比差异选择模式 newDiff unDiffAll
let unDiffAll = () => {};
function newDiff(originTxt) {
unDiffAll();
document.querySelectorAll('.original.mb-1 span + a, .pz-diff-cog').forEach(a => a.remove());
const strings = document.querySelectorAll('.original.mb-1 span');
const { $diff } = document.querySelector('main').__vue__;
for (const string of strings) {
string.oriHTML = string.innerHTML;
const strText = string.textContent;
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>';
const diffCog = makeDiffCog(() => {
string.innerHTML = diffByMode();
string.isShown = true;
});
string.after(' ', showDiff, ' ', diffCog);
showDiff.addEventListener('click', function(e) {
e.preventDefault();
string.innerHTML = string.isShown ? string.oriHTML : diffByMode();
string.isShown = !string.isShown;
});
function diffByMode() {
return $diff(strText, originTxt, { mode: localStorage.getItem('pzdiffmode') || 'auto' });
}
}
unDiffAll = () => {
[...strings].filter(string => string.isShown).forEach(string => string.innerHTML = string.oriHTML);
};
}
function makeDiffCog(onDiffModeChange) {
const button = document.createElement('a');
button.href = '#';
button.className = 'hover mr-1 pz-diff-cog';
button.innerHTML = '<i aria-hidden="true" class="far fa-cog"></i>';
// 从 localStorage 获取配置
const savedMode = localStorage.getItem('pzdiffmode') || 'auto';
// 定义选项配置
const modeOptions = [
{ value: 'auto', label: '自动' },
{ value: 'chars', label: '按字' },
{ value: 'words', label: '按词' },
{ value: 'sentences', label: '按句' }
];
// 生成单选按钮组 HTML
const radioButtons = modeOptions.map(option => `
<div class="custom-control custom-radio">
<input type="radio" class="custom-control-input"
value="${option.value}"
id="diff${option.value.charAt(0).toUpperCase() + option.value.slice(1)}"
name="diffMode"
${savedMode === option.value ? 'checked' : ''}>
<label class="custom-control-label"
for="diff${option.value.charAt(0).toUpperCase() + option.value.slice(1)}">
${option.label}
</label>
</div>
`).join('');
// 创建菜单元素
const menu = document.createElement('div');
menu.className = 'popover b-popover bs-popover-bottom';
menu.style.cssText = 'position: absolute; will-change: transform;';
menu.innerHTML = /* html */`
<div class="arrow"></div>
<div class="popover-body">
<fieldset class="form-group" id="diffModeGroup">
<legend tabindex="-1" class="bv-no-focus-ring col-form-label pt-0">差分模式</legend>
<div>
<div role="radiogroup" tabindex="-1" class="bv-no-focus-ring">
${radioButtons}
</div>
</div>
</fieldset>
</div>
`;
// 菜单位置计算
function positionMenu() {
const buttonRect = button.getBoundingClientRect();
const menuWidth = menu.offsetWidth;
const buttonCenterX = buttonRect.left + buttonRect.width / 2 + window.scrollX;
menu.style.top = `${buttonRect.bottom + window.scrollY}px`;
menu.style.left = `${buttonCenterX - menuWidth / 2}px`;
const arrow = menu.querySelector('.arrow');
const arrowWidth = arrow.offsetWidth;
arrow.style.left = `${(menu.offsetWidth - arrowWidth) / 2}px`;
}
// 状态管理
let isMenuVisible = false;
let isMenuLocked = false;
let menuTimer = null;
// 显示菜单
function showMenu() {
if (!document.body.contains(menu)) {
document.body.appendChild(menu);
}
positionMenu();
isMenuVisible = true;
button.classList.add('active');
}
// 隐藏菜单
function hideMenu() {
if (document.body.contains(menu)) {
menu.remove();
isMenuVisible = false;
isMenuLocked = false;
button.classList.remove('active');
}
}
// 悬浮事件 - 修改这里
button.addEventListener('mouseenter', () => {
if (!isMenuLocked) {
showMenu();
}
clearTimeout(menuTimer);
});
// 修改这里:当鼠标离开按钮时,如果菜单可见且没有被锁定,则设置延迟隐藏
button.addEventListener('mouseleave', () => {
if (!isMenuLocked && isMenuVisible) {
menuTimer = setTimeout(hideMenu, 500);
}
});
// 菜单的鼠标进入事件 - 新增
menu.addEventListener('mouseenter', () => {
if (isMenuVisible) {
clearTimeout(menuTimer); // 清除延迟隐藏的计时器
}
});
// 菜单的鼠标离开事件 - 新增
menu.addEventListener('mouseleave', () => {
if (isMenuVisible && !isMenuLocked) {
menuTimer = setTimeout(hideMenu, 500); // 设置延迟隐藏
}
});
// 点击事件
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isMenuLocked = !isMenuLocked;
if (isMenuLocked) {
showMenu();
} else {
hideMenu();
}
});
// 点击外部关闭
document.addEventListener('click', (e) => {
if (isMenuVisible && !button.contains(e.target) && !menu.contains(e.target)) {
hideMenu();
}
});
// 滚动更新位置
window.addEventListener('scroll', () => {
if (isMenuVisible) {
positionMenu();
}
}, true);
// 模式变更处理
menu.querySelectorAll('input[name="diffMode"]').forEach(input => {
input.addEventListener('change', () => {
localStorage.setItem('pzdiffmode', input.value);
onDiffModeChange();
});
});
return button;
}
// #region 自动保存全部相同词条 autoSaveAll
const autoSave = localStorage.getItem('pzdiffautosave');
function autoSaveAll() {
const button = document.querySelector('.modal-dialog .btn-primary');
if (autoSave && button.textContent === '保存全部') button.click();
}
// #endregion
// #region 自动填充100%相似译文 autoFill100(suggests, originTxt)
function autoFill100(suggests, originTxt) {
if (!suggests[0]) return;
const getSim = (suggest) => +suggest.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
const getTranslation = (suggest) => suggest.querySelector('.translation').firstChild.textContent;
for (const suggest of suggests) {
const sim = getSim(suggest);
const equalOrigin = [100, 101].includes(sim) || isEqualWithOneCharDifference(originTxt, getOriginalFromSuggest(suggest));
if (equalOrigin) {
mockInput(getTranslation(suggest));
break;
}
}
}
function isEqualWithOneCharDifference(str1, str2) {
if (str1 === str2) return true;
if (Math.abs(str1.length - str2.length) > 1) return false;
let differences = 0;
const len1 = str1.length;
const len2 = str2.length;
let i = 0, j = 0;
while (i < len1 && j < len2) {
if (str1[i] !== str2[j]) {
differences++;
if (differences > 1) return false;
if (len1 > len2) i++;
else if (len2 > len1) j++;
else {
i++;
j++;
}
} else {
i++;
j++;
}
}
if (i < len1 || j < len2) differences++;
return differences <= 1;
}
// #endregion
// #region 重新排序历史词条 findTextWithin(suggests, originTxt) getDefaultSorted addReSortBtn
function findTextWithin(suggests, originTxt, searching = false) {
if (!suggests[0]) return;
originTxt = normalizeString(originTxt);
const getHeaderSpanIfNotSame = searching ? (header, original) => {
const headerSpan = document.createElement('span');
header.prepend(headerSpan);
if (original === originTxt) {
headerSpan.textContent = '匹配率 100%';
return null;
}
return headerSpan;
} : (header) => {
return header.querySelector('span');
};
for (const suggest of suggests) {
const suggestOri = getOriginalFromSuggest(suggest);
const header = suggest.querySelector('header');
let headerSpan;
if (!(headerSpan = getHeaderSpanIfNotSame(header, suggestOri))) continue;
if (suggestOri.includes(originTxt)) {
suggest.parentNode.prepend(suggest);
if (headerSpan.textContent.includes('100%') || headerSpan.textContent.includes('101%')) return;
headerSpan.textContent = '文本在中';
}
}
}
const reSortSuggests = (compareFn) => (suggests) => {
if (!suggests[0]) return;
const sorted = [...suggests].sort(compareFn);
const parent = suggests[0].parentNode;
const frag = document.createDocumentFragment();
frag.append(...sorted);
parent.innerHTML = '';
parent.appendChild(frag);
};
const reSortSuggestsBySim = reSortSuggests((a, b) => {
const getSim = (suggest) => {
const simContainer = suggest.querySelector('header span');
if (!simContainer) return 102;
const sim = +simContainer.textContent.split('\n')?.[2]?.trim().slice(0, -1);
if (!sim) return 99.999; // 在文本中
return sim;
}
return getSim(b) - getSim(a);
});
const reSortSuggestsByTime = reSortSuggests((a, b) => {
const getTimestamp = (suggest) => {
const time = suggest.querySelector('time')?.dateTime;
if (!time) return Infinity;
return +new Date(time);
}
return getTimestamp(b) - getTimestamp(a);
});
const reSortSuggestsByMem = (suggests) => {
const sortType = localStorage.getItem('pzdiffsort') || 'sim';
if (sortType === 'sim') {
reSortSuggestsBySim(suggests);
} else if (sortType === 'time') {
reSortSuggestsByTime(suggests);
}
};
let defaultSortedSuggests = [];
const getDefaultSorted = (suggests, searching = false) => {
defaultSortedSuggests[+searching] = suggests;
};
function recoverDefaultSort() {
const parent = document.querySelector('.translation-memory .list:not(.mt-list)');
if (!parent) return;
const searching = !!document.querySelector('.search-form');
parent.append(...defaultSortedSuggests[+searching]);
}
function addReSortBtn() {
if (document.querySelector('.pzdiffsort')) return;
const btn = document.createElement('a');
btn.href = 'javascript:';
btn.className = 'pzdiffsort';
const icon = type => {
const icon = document.createElement('i');
icon.classList.add('far', `fa-${type}`);
icon.ariaHidden = true;
return icon;
}
const simBtn = btn.cloneNode();
simBtn.title = '按相似度排序';
simBtn.append(icon('percentage'));
simBtn.addEventListener('click', () => {
const suggests = document.querySelectorAll('.string-item');
reSortSuggestsByTime(suggests);
localStorage.setItem('pzdiffsort', 'time');
simBtn.replaceWith(timeBtn);
});
const timeBtn = btn.cloneNode();
timeBtn.title = '按时间排序';
timeBtn.append(icon('history'));
timeBtn.addEventListener('click', () => {
recoverDefaultSort();
localStorage.setItem('pzdiffsort', 'default');
timeBtn.replaceWith(defaultBtn);
});
const defaultBtn = btn.cloneNode();
defaultBtn.title = '默认排序';
defaultBtn.append(icon('sort-amount-down'));
defaultBtn.addEventListener('click', () => {
const suggests = document.querySelectorAll('.string-item');
reSortSuggestsBySim(suggests);
localStorage.setItem('pzdiffsort', 'sim');
defaultBtn.replaceWith(simBtn);
});
const sortType = localStorage.getItem('pzdiffsort') || 'sim';
const initBtn = {
sim: simBtn,
time: timeBtn,
default: defaultBtn,
};
document.querySelector('.translation-memory .col-auto').after(initBtn[sortType]);
}
// #endregion
// #region 修改历史词条按钮 tweakHist
function tweakHist() {
addCopySearchBtn();
addReSortBtn();
}
// #endregion
// #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] ?? 1
];
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] ?? 1) !== 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;
// eslint-disable-next-line no-unused-vars
const original = originElem.textContent;
// eslint-disable-next-line no-unused-vars
const oldTrans = document.querySelector('textarea.translation').value;
let suggest = null, suggestSim = 0;
if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) {
const suggestEle = (await waitForElems('.translation-memory .string-item .translation, .empty-sign'))[0];
if (suggestEle.classList.contains('empty-sign')) {
if (skipOrFin(originElem, nextButton)) continue; else break;
}
// eslint-disable-next-line no-unused-vars
suggest = suggestEle.textContent;
suggestSim = +suggestEle.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
if ((translationPattern.includes('suggestSim') || skipPattern.includes('suggestSim')) && isNaN(suggestSim)) {
if (skipOrFin(originElem, nextButton)) continue; else break;
}
}
// eslint-disable-next-line no-unused-vars
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
// #region 函数调用逻辑
addHotkeys();
initAuto();
let stringPageTurned = true;
async function actByPath(path) {
if (path.split('/').pop() === 'strings') {
let originalEle;
let lastOriginHTML = '';
let toObserve = document.body;
const observer = new MutationObserver((mutations) => {
if (shouldSkip()) return;
originalEle = document.querySelector('.editor-core .original');
if (!originalEle) return;
const originUpded = originalEle.innerHTML !== lastOriginHTML;
lastOriginHTML = originalEle.innerHTML;
observer.disconnect();
initDiffClick();
extractDiff();
const markAll = () => {
// fixOriginTwins(original);
removeContextTags();
markSearchParams();
markContext(originalEle.textContent);
};
if (stringPageTurned) {
if (!originUpded) {
connectObserve();
return;
}
console.debug('framework loaded');
initDropMark();
initMarkJS();
tweakHist();
watchContextBtn();
markAll();
tweakButtons();
stringPageTurned = false;
connectObserve();
return;
}
if (originUpded) {
console.debug('origin upded');
cancelSearchResult();
markAll();
if (!document.getElementById('PZpaste')) tweakButtons(); // 防止他人占用按钮消失
}
mutations = mutations.filter(({ addedNodes, removedNodes }) => !(
[addedNodes[0]?.nodeName, removedNodes[0]?.nodeName].includes('#comment') ||
(addedNodes.length === 1 && (addedNodes[0].classList?.contains('square-image') ||
addedNodes[0].classList?.contains('fa-copy') ||
addedNodes[0].href === 'javascript:'))
|| (removedNodes.length === 1 && removedNodes[0].classList?.contains('fa-copy'))
));
for (const mutation of mutations) {
const { addedNodes, removedNodes } = mutation;
// console.debug({ addedNodes, removedNodes });
if (addedNodes.length === 1) {
const node = addedNodes[0];
// if (node.matches?.('.list-group.tags')) {
// updFixedTags();
// continue;
// }
if (node.matches?.('.string-item a.small')) {
node.remove();
continue;
}
if (node.matches?.('.modal-backdrop')) {
autoSaveAll();
continue;
}
if (node.classList?.contains('search-form')) {
console.debug('search expanded');
renderSearchResult(originalEle.textContent);
newDiff(originalEle.textContent);
continue;
}
if (node.classList?.contains('translation-memory')) {
console.debug('suggests layout switched');
tweakHist();
}
// if (node.classList?.contains('results')) {
// console.debug('terms loaded');
// fixOriginTerms(original);
// }
// if (node.classList?.contains('ghost-textarea')) {
// fixOriginTwins(node);
// continue;
// }
} else if (removedNodes.length === 1) {
const node = removedNodes[0];
if (mutation.target.classList?.contains('translation-memory')
&& node.classList?.contains('loading')) {
console.debug('suggests loaded');
const suggests = document.querySelectorAll('.string-item');
const searchInput = document.querySelector('.search-form input');
if (searchInput) {
inPageKeyword = searchInput.value;
console.debug('search loaded: ', inPageKeyword);
renderSearchResult(originalEle.textContent);
}
findTextWithin(suggests, originalEle.textContent, !!searchInput);
getDefaultSorted(suggests, !!searchInput);
autoFill100(suggests, originalEle.textContent);
reSortSuggestsByMem(suggests);
newDiff(originalEle.textContent);
continue;
} else if (node.classList?.contains('search-form')) {
console.debug('search collapsed');
const suggests = document.querySelectorAll('.string-item');
resultMark?.unmark?.();
findTextWithin(suggests, originalEle.textContent);
reSortSuggestsByMem(suggests);
newDiff(originalEle.textContent);
}
// if (node.matches?.('.list-group.tags')) {
// tagSelectController.abort();
// continue;
// }
}
// if (addedNodes[0]?.parentElement?.classList.contains('ghost-textarea')) {
// fixOriginTwins(document.querySelector('.ghost-textarea'));
// }
}
connectObserve();
});
connectObserve();
function connectObserve() {
observer.observe(toObserve, {
childList: true,
subtree: true,
});
}
return observer;
} else if (path.split('/').at(-2) === 'issues') {
waitForElems('.text-content p img').then((imgs) => imgs.forEach(mediumZoom));
} else if (path.split('/').pop() === 'history') {
let observer = new MutationObserver(() => {
observer.disconnect();
extractHist();
connectObserve();
});
connectObserve();
function connectObserve() {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
return observer;
}
}
let cancelAct = await actByPath(location.pathname);
(await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async (to, from) => {
dropLastTextareaMark?.();
if (JSON.stringify(to.query) !== JSON.stringify(from.query)) {
console.debug('query changed');
if (to.path.split('/').pop() === 'strings') {
stringPageTurned = true;
}
}
if (to.path === from.path) return;
// tagSelectController.abort();
cancelAct?.disconnect();
console.debug('path changed');
cancelAct = await actByPath(to.path);
});
// #endregion
// #region 通用工具函数
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;
textarea.focus();
}
function normalizeString(str) {
if (!str) return '';
return str
.replace(/[,.;'"-]/g, '')
.replace(/\s+/g, '')
.toLowerCase();
}
function getOriginalFromSuggest(suggest) {
return normalizeString(suggest.querySelector('.original')?.firstChild.textContent);
}
// #endregion
})();