ParaTranz diff

ParaTranz enhanced

目前为 2025-01-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ParaTranz diff
  3. // @namespace https://paratranz.cn/users/44232
  4. // @version 0.11.1
  5. // @description ParaTranz enhanced
  6. // @author ooo
  7. // @match http*://paratranz.cn/*
  8. // @icon https://paratranz.cn/favicon.png
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/medium-zoom/1.1.0/medium-zoom.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (async function() {
  15. 'use strict';
  16.  
  17. // #region 主要功能函数
  18.  
  19. // #region 自动跳过空白页 shouldSkip
  20. function shouldSkip() {
  21. if (document.querySelector('.string-list .empty-sign') &&
  22. location.search.match(/(\?|&)page=\d+/g)) {
  23. document.querySelector('.pagination .page-item a')?.click();
  24. return true;
  25. }
  26. }
  27. // #endregion
  28.  
  29. // #region 添加快捷键 addHotkeys
  30. function addHotkeys() {
  31. document.addEventListener('keydown', (e) => {
  32. if (e.ctrlKey && e.shiftKey && e.key === 'V') {
  33. e.preventDefault();
  34. mockInput(document.querySelector('.editor-core .original')?.textContent);
  35. }
  36. });
  37. }
  38. // #endregion
  39.  
  40. // #region 更多搜索高亮 initDropMark markSearchParams initMarkJS watchContextBtn
  41. let markSearchParams = () => {};
  42. const mergeObjects = (obj1, obj2) => {
  43. const merged = {};
  44. for (const key of Object.keys(obj1)) {
  45. merged[key] = [...obj1[key], ...obj2[key]];
  46. }
  47. return merged;
  48. };
  49. function updMark() {
  50. const params = new URLSearchParams(location.search);
  51. const getParams = (type) => {
  52. return {
  53. contains: [...params.getAll(type)],
  54. startsWith: [...params.getAll(`${type}^`)],
  55. endsWith: [...params.getAll(`${type}$`)],
  56. match: [...params.getAll(`${type}~`)]
  57. };
  58. };
  59. const texts = getParams('text');
  60. const originals = getParams('original');
  61. const translations = getParams('translation');
  62. const contexts = getParams('context');
  63.  
  64. const originKeywords = mergeObjects(texts, originals);
  65. const editingKeywords = mergeObjects(texts, translations);
  66. const contextKeywords = contexts;
  67.  
  68. markSearchParams = () => {
  69. markOrigin(originKeywords);
  70. markContext(contextKeywords);
  71. }
  72.  
  73. if (Object.values(editingKeywords).filter(v => v).length) {
  74. const dropMark = markEditing(editingKeywords);
  75. return dropMark;
  76. }
  77. }
  78. let dropLastTextareaMark;
  79. const initDropMark = () => dropLastTextareaMark = updMark();
  80.  
  81. let originMark;
  82. let contextMark;
  83.  
  84. function initMarkJS() {
  85. const original = document.querySelector('.editor-core .original');
  86. originMark = new Mark(original);
  87.  
  88. original.addEventListener('click', (e) => {
  89. if (e.target.tagName === 'MARK') {
  90. const originalElement = e.target.parentElement;
  91. originalElement.click();
  92. }
  93. });
  94.  
  95. const context = document.querySelector('.context');
  96. if (context) contextMark = new Mark(context);
  97. }
  98.  
  99. function watchContextBtn() {
  100. const btn = document.querySelector('.float-right a');
  101. if (!btn) return;
  102. btn.addEventListener('click', () => {
  103. const context = document.querySelector('.context');
  104. if (!context) return;
  105. removeContextTags();
  106.  
  107. const original = document.querySelector('.editor-core .original').textContent;
  108. contextMark = new Mark(context);
  109. markContext(original);
  110. });
  111. }
  112.  
  113. function mark(target, keywords, options) {
  114. if (!target) return;
  115. target.unmark();
  116.  
  117. const caseSensitive = !document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked;
  118. const flags = caseSensitive ? 'g' : 'ig';
  119.  
  120. const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  121.  
  122. const { contains, startsWith, endsWith, match } = keywords;
  123.  
  124. const patterns = [
  125. ...contains.map(keyword => `(${escapeRegExp(keyword)})`),
  126. ...startsWith.map(keyword => `^(${escapeRegExp(keyword)})`),
  127. ...endsWith.map(keyword => `(${escapeRegExp(keyword)})$`),
  128. match
  129. ].filter(p => p.length).join('|');
  130.  
  131. if (patterns) {
  132. const regex = new RegExp(patterns, flags);
  133. target.markRegExp(regex, {
  134. acrossElements: true,
  135. separateWordSearch: false,
  136. ...options
  137. });
  138. }
  139. }
  140.  
  141. function markOrigin(keywords) {
  142. mark(originMark, keywords);
  143. }
  144.  
  145. function markContext(originTxt) {
  146. mark(contextMark, originTxt, { className: 'mark'});
  147. }
  148.  
  149. function markEditing(keywords) {
  150. let textarea = document.querySelector('textarea.translation');
  151. if (!textarea) return;
  152. const lastOverlay = document.getElementById('PZSoverlay');
  153. if (lastOverlay) return;
  154.  
  155. const overlay = document.createElement('div');
  156. overlay.id = 'PZSoverlay';
  157. overlay.className = textarea.className;
  158. const textareaStyle = window.getComputedStyle(textarea);
  159. for (let i = 0; i < textareaStyle.length; i++) {
  160. const property = textareaStyle[i];
  161. overlay.style[property] = textareaStyle.getPropertyValue(property);
  162. }
  163. overlay.style.position = 'absolute';
  164. overlay.style.pointerEvents = 'none';
  165. overlay.style.setProperty('background', 'transparent', 'important');
  166. overlay.style['-webkit-text-fill-color'] = 'transparent';
  167. overlay.style.overflowY = 'hidden';
  168. overlay.style.resize = 'none';
  169.  
  170. textarea.parentNode.appendChild(overlay);
  171.  
  172. const updOverlay = () => {
  173. overlay.innerText = textarea.value;
  174. const fillColor = window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color');
  175. mark(new Mark(overlay), keywords, {
  176. each: (m) => {
  177. m.style['-webkit-text-fill-color'] = fillColor;
  178. m.style.opacity = .5;
  179. }
  180. });
  181. overlay.style.top = textarea.offsetTop + 'px';
  182. overlay.style.left = textarea.offsetLeft + 'px';
  183. overlay.style.width = textarea.offsetWidth + 'px';
  184. overlay.style.height = textarea.offsetHeight + 'px';
  185. };
  186.  
  187. updOverlay();
  188.  
  189. textarea.addEventListener('input', updOverlay);
  190.  
  191. const observer = new MutationObserver(updOverlay);
  192. observer.observe(textarea, { attributes: true });
  193.  
  194. window.addEventListener('resize', updOverlay);
  195.  
  196. const cancelOverlay = () => {
  197. observer.disconnect();
  198. textarea.removeEventListener('input', updOverlay);
  199. window.removeEventListener('resize', updOverlay);
  200. overlay.remove();
  201. }
  202. return cancelOverlay;
  203. }
  204. // #endregion
  205.  
  206. // #region 修复原文排版崩坏和<<>> fixOrigin(originElem)
  207. function fixOrigin(originElem) {
  208. originElem.innerHTML = originElem.innerHTML
  209. .replaceAll('<abbr title="noun.>" data-value=">">&gt;</abbr>', '&gt;')
  210. .replaceAll(/<var>(&lt;&lt;[^<]*?&gt;)<\/var>&gt;/g, '<var>$1&gt;</var>')
  211. .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">&gt;&gt;', '')
  212. .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;&gt;', '')
  213. .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;', '');
  214. }
  215. // #endregion
  216.  
  217. // #region 还原上下文HTML源码 removeContextTags
  218. function removeContextTags() {
  219. const context = document.querySelector('.context');
  220. if (!context) return;
  221. context.innerHTML = context.innerHTML.replace(/<a.*?>(.*?)<\/a>/g, '$1').replace(/<(\/?)(li|b|i|u|h\d|span)>/g, '&lt;$1$2&gt;');
  222. }
  223. // #endregion
  224.  
  225. // #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect
  226. const insertTag = debounce(async (tag) => {
  227. const textarea = document.querySelector('textarea.translation');
  228. const startPos = textarea.selectionStart;
  229. const endPos = textarea.selectionEnd;
  230. const currentText = textarea.value;
  231.  
  232. const before = currentText.slice(0, startPos);
  233. const after = currentText.slice(endPos);
  234.  
  235. await mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after.slice(0, -2)); // -2 去除\n
  236.  
  237. textarea.selectionStart = startPos + 1;
  238. textarea.selectionEnd = endPos + 1;
  239. })
  240.  
  241. let activeTag = null;
  242. let modifiedTags = [];
  243. const tagSelectController = new AbortController();
  244. const { tagSelectSignal } = tagSelectController;
  245.  
  246. function tagSelectHandler(e) {
  247. if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
  248. activeTag &&= document.querySelector('.list-group-item.tag.active');
  249. }
  250. if (e.key === 'Enter') {
  251. if (!activeTag) return;
  252. if (!modifiedTags.includes(activeTag)) return;
  253. e.preventDefault();
  254. insertTag(activeTag?.textContent);
  255. activeTag = null;
  256. }
  257. }
  258.  
  259. function updFixedTags() {
  260.  
  261. const tags = document.querySelectorAll('.list-group-item.tag');
  262. activeTag = document.querySelector('.list-group-item.tag.active');
  263. modifiedTags = [];
  264.  
  265. for (const tag of tags) {
  266. tag.innerHTML = tag.innerHTML.trim();
  267. if (tag.innerHTML.startsWith('&lt;&lt;') && !tag.innerHTML.endsWith('&gt;&gt;')) {
  268. tag.innerHTML += '&gt;';
  269. modifiedTags.push(tag);
  270. }
  271. }
  272.  
  273. document.addEventListener('keyup', tagSelectHandler, { tagSelectSignal });
  274.  
  275. }
  276. // #endregion
  277.  
  278. // #region 将填充原文移到右边,增加填充原文并保存 tweakButtons
  279. function tweakButtons() {
  280. const copyButton = document.querySelector('button.btn-secondary:has(.fa-clone)');
  281. const rightButtons = document.querySelector('.right .btn-group');
  282.  
  283. if (rightButtons) {
  284. if (copyButton) {
  285. rightButtons.insertBefore(copyButton, rightButtons.firstChild);
  286. }
  287. if (document.querySelector('#PZpaste')) return;
  288. const pasteSave = document.createElement('button');
  289. rightButtons.appendChild(pasteSave);
  290. pasteSave.id = 'PZpaste';
  291. pasteSave.type = 'button';
  292. pasteSave.classList.add('btn', 'btn-secondary');
  293. pasteSave.title = '填充原文并保存';
  294. pasteSave.innerHTML = '<i aria-hidden="true" class="far fa-save"></i>';
  295. pasteSave.addEventListener('click', async () => {
  296. await mockInput(document.querySelector('.editor-core .original')?.textContent);
  297. document.querySelector('.right .btn-primary')?.click();
  298. });
  299. }
  300. }
  301. // #endregion
  302.  
  303. // #region 缩略对比差异中过长无差异文本 extractDiff
  304. function extractDiff() {
  305. document.querySelectorAll('.diff-wrapper:not(.PZedited)').forEach(wrapper => {
  306. wrapper.childNodes.forEach(node => {
  307. if (node.nodeType !== Node.TEXT_NODE || node.length < 200) return;
  308.  
  309. const text = node.cloneNode();
  310. const expand = document.createElement('span');
  311. expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`;
  312. expand.style.cursor = 'pointer';
  313. expand.style.background = 'linear-gradient(to right, transparent, #aaf6, transparent)';
  314. expand.style.borderRadius = '2px';
  315.  
  316. let time = 0;
  317. let isMoving = false;
  318.  
  319. const start = () => {
  320. time = Date.now()
  321. isMoving = false;
  322. }
  323. const end = () => {
  324. if (isMoving || Date.now() - time > 500) return;
  325. expand.replaceWith(text);
  326. }
  327.  
  328. expand.addEventListener('mousedown', start);
  329. expand.addEventListener('mouseup', end);
  330. expand.addEventListener('mouseleave', () => time = 0);
  331.  
  332. expand.addEventListener('touchstart', start);
  333. expand.addEventListener('touchend', end);
  334. expand.addEventListener('touchcancel', () => time = 0);
  335. expand.addEventListener('touchmove', () => isMoving = true);
  336.  
  337. node.replaceWith(expand);
  338. });
  339. wrapper.classList.add('PZedited');
  340. });
  341. }
  342. // #endregion
  343.  
  344. // #region 点击对比差异绿色文字粘贴其中文本 initDiffClick
  345. function initDiffClick() {
  346. const addeds = document.querySelectorAll('.diff.added:not(.PZPedited)');
  347. for (const added of addeds) {
  348. added.classList.add('PZPedited');
  349. const text = added.textContent.replaceAll('\\n', '\n');
  350. added.style.cursor = 'pointer';
  351. added.addEventListener('click', () => {
  352. mockInsert(text);
  353. });
  354. }
  355. }
  356. // #endregion
  357.  
  358. // #region 快速搜索原文 addCopySearchBtn
  359. async function addCopySearchBtn() {
  360. if (document.querySelector('#PZsch')) return;
  361. const originSch = document.querySelector('.btn-sm');
  362. if (!originSch) return;
  363. originSch.insertAdjacentHTML('beforebegin', '<button id="PZsch" type="button" class="btn btn-secondary btn-sm"><i aria-hidden="true" class="far fa-paste"></i></button>');
  364. const newSch = document.querySelector('#PZsch');
  365. newSch.addEventListener('click', async () => {
  366. const original = document.querySelector('.editor-core .original')?.textContent;
  367. let input = document.querySelector('.search-form input[type=search]');
  368. if (!input) {
  369. await (() => new Promise(resolve => resolve(originSch.click())))();
  370. input = document.querySelector('.search-form input[type=search]');
  371. }
  372. const submit = document.querySelector('.search-form button');
  373. await (() => new Promise(resolve => {
  374. input.value = original;
  375. input.dispatchEvent(new Event('input', {
  376. bubbles: true,
  377. cancelable: true,
  378. }));
  379. resolve();
  380. }))();
  381. submit.click();
  382. });
  383. }
  384. // #endregion
  385.  
  386. // #region 进入下一条时关闭搜索结果 cancelSearchResult
  387. function cancelSearchResult() {
  388. const input = document.querySelector('.search-form input[type=search]');
  389. if (input) document.querySelectorAll('.btn-sm')[1]?.click();
  390. }
  391. // #endregion
  392.  
  393. // #region 搜索结果对比差异 initSearchResultDiff(originTxt)
  394. function initSearchResultDiff(originTxt) {
  395. const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)');
  396. if (!strings[0]) return;
  397.  
  398. const { $diff } = document.querySelector('main').__vue__;
  399.  
  400. for (const string of strings) {
  401. const strHTML = string.innerHTML;
  402. const showDiff = document.createElement('a');
  403. showDiff.title = '查看差异';
  404. showDiff.href = '#';
  405. showDiff.target = '_self';
  406. showDiff.classList.add('small');
  407. showDiff.innerHTML = '<i aria-hidden="true" class="far fa-right-left-large"></i>';
  408.  
  409. string.after(' ', showDiff);
  410. showDiff.addEventListener('click', function(e) {
  411. e.preventDefault();
  412. string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, originTxt);
  413. this.isShown = !this.isShown;
  414. });
  415. }
  416. }
  417. // #endregion
  418.  
  419. // #region 高级搜索空格变+修复 fixAdvSch
  420. function fixAdvSch() {
  421. const inputs = document.querySelectorAll('#advancedSearch table input');
  422. if (!inputs[0]) return;
  423. const params = new URLSearchParams(location.search);
  424. const values = [...params.entries()].filter(([key, _]) => /(text|original|translation).?/.test(key)).map(([_, value]) => value.replaceAll(' ', '+'));
  425. for (const input of inputs) {
  426. if (values.includes(input.value)) {
  427. input.value = input.value.replaceAll('+', ' ');
  428. input.dispatchEvent(new Event('input', {
  429. bubbles: true,
  430. cancelable: true,
  431. }));
  432. }
  433. }
  434. }
  435. // #endregion
  436.  
  437. // #region 自动保存全部相同词条 autoSaveAll
  438. const autoSave = localStorage.getItem('pzdiffautosave');
  439. function autoSaveAll() {
  440. const button = document.querySelector('.modal-dialog .btn-primary');
  441. if (autoSave && button.textContent === '保存全部') button.click();
  442. }
  443. // #endregion
  444.  
  445. // #region 自动填充100%相似译文 autoFill100(suggests, originTxt)
  446. function autoFill100(suggests, originTxt) {
  447. if (!suggests[0]) return;
  448. const getSim = (suggest) => +suggest.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
  449. const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent);
  450. const getTranslation = (suggest) => suggest.querySelector('.translation').firstChild.textContent;
  451. for (const suggest of suggests) {
  452. const sim = getSim(suggest);
  453. const equalOrigin = [100, 101].includes(sim) || isEqualWithOneCharDifference(originTxt, getOriginal(suggest));
  454. if (equalOrigin) {
  455. mockInput(getTranslation(suggest));
  456. break;
  457. }
  458. }
  459. }
  460.  
  461. function isEqualWithOneCharDifference(str1, str2) {
  462. if (str1 === str2) return true;
  463.  
  464. if (Math.abs(str1.length - str2.length) > 1) return false;
  465.  
  466. let differences = 0;
  467. const len1 = str1.length;
  468. const len2 = str2.length;
  469. let i = 0, j = 0;
  470.  
  471. while (i < len1 && j < len2) {
  472. if (str1[i] !== str2[j]) {
  473. differences++;
  474. if (differences > 1) return false;
  475.  
  476. if (len1 > len2) i++;
  477. else if (len2 > len1) j++;
  478. else {
  479. i++;
  480. j++;
  481. }
  482. } else {
  483. i++;
  484. j++;
  485. }
  486. }
  487.  
  488. if (i < len1 || j < len2) differences++;
  489.  
  490. return differences <= 1;
  491. }
  492. // #endregion
  493.  
  494. // #region 重新排序历史词条 findTextWithin(suggests, originTxt) getDefaultSorted addReSortBtn
  495. function findTextWithin(suggests, originTxt) {
  496. if (!suggests[0]) return;
  497. originTxt = normalizeString(originTxt);
  498. const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent);
  499. for (const suggest of suggests) {
  500. if (getOriginal(suggest).includes(originTxt)) {
  501. suggest.parentNode.prepend(suggest);
  502. const header = suggest.querySelector('header');
  503. let headerSpan = header.querySelector('span');
  504. if (!headerSpan) {
  505. headerSpan = document.createElement('span');
  506. header.prepend(headerSpan);
  507. }
  508. if (headerSpan.textContent.includes('100%') || headerSpan.textContent.includes('101%')) break;
  509. headerSpan.textContent = '文本在中';
  510. break;
  511. }
  512. }
  513. }
  514.  
  515. const reSortSuggests = (compareFn) => (suggests) => {
  516. if (!suggests[0]) return;
  517. const sorted = [...suggests].sort(compareFn);
  518. const parent = suggests[0].parentNode;
  519. const frag = document.createDocumentFragment();
  520. frag.append(...sorted);
  521. parent.innerHTML = '';
  522. parent.appendChild(frag);
  523. };
  524.  
  525. const reSortSuggestsBySim = reSortSuggests((a, b) => {
  526. const getSim = (suggest) => {
  527. const simContainer = suggest.querySelector('header span');
  528. if (!simContainer) return 102; // 机器翻译参考
  529. const sim = +simContainer.textContent.split('\n')?.[2]?.trim().slice(0, -1);
  530. if (!sim) return 102; // 在文本中
  531. return sim;
  532. }
  533. return getSim(b) - getSim(a);
  534. });
  535.  
  536. const reSortSuggestsByTime = reSortSuggests((a, b) => {
  537. const getTimestamp = (suggest) => {
  538. const time = suggest.querySelector('time')?.dateTime;
  539. if (!time) return Infinity;
  540. return +new Date(time);
  541. }
  542. return getTimestamp(b) - getTimestamp(a);
  543. });
  544.  
  545. const reSortSuggestsByMem = (suggests) => {
  546. const sortType = localStorage.getItem('pzdiffsort') || 'sim';
  547. if (sortType === 'sim') {
  548. reSortSuggestsBySim(suggests);
  549. } else if (sortType === 'time') {
  550. reSortSuggestsByTime(suggests);
  551. }
  552. };
  553.  
  554. let defaultSortedSuggests = [];
  555. const getDefaultSorted = (suggests) => {
  556. defaultSortedSuggests = suggests;
  557. };
  558. function recoverDefaultSort() {
  559. const parent = document.querySelector('.translation-memory .list');
  560. if (!parent) return;
  561. parent.append(...defaultSortedSuggests);
  562. }
  563.  
  564. function addReSortBtn() {
  565. if (document.querySelector('.pzdiffsort')) return;
  566. const btn = document.createElement('a');
  567. btn.href = 'javascript:';
  568. btn.className = 'pzdiffsort';
  569. const icon = type => {
  570. const icon = document.createElement('i');
  571. icon.classList.add('far', `fa-${type}`);
  572. icon.ariaHidden = true;
  573. icon.style.cursor = 'pointer';
  574. return icon;
  575. }
  576.  
  577. const simBtn = btn.cloneNode();
  578. simBtn.title = '按相似度排序';
  579. simBtn.append(icon('percentage'));
  580. simBtn.addEventListener('click', () => {
  581. const suggests = document.querySelectorAll('.string-item');
  582. reSortSuggestsByTime(suggests);
  583. localStorage.setItem('pzdiffsort', 'time');
  584. simBtn.replaceWith(timeBtn);
  585. });
  586.  
  587. const timeBtn = btn.cloneNode();
  588. timeBtn.title = '按时间排序';
  589. timeBtn.append(icon('history'));
  590. timeBtn.addEventListener('click', () => {
  591. recoverDefaultSort();
  592. localStorage.setItem('pzdiffsort', 'default');
  593. timeBtn.replaceWith(defaultBtn);
  594. });
  595.  
  596. const defaultBtn = btn.cloneNode();
  597. defaultBtn.title = '默认排序';
  598. defaultBtn.append(icon('sort-amount-down'));
  599. defaultBtn.addEventListener('click', () => {
  600. const suggests = document.querySelectorAll('.string-item');
  601. reSortSuggestsBySim(suggests);
  602. localStorage.setItem('pzdiffsort', 'sim');
  603. defaultBtn.replaceWith(simBtn);
  604. });
  605.  
  606. const sortType = localStorage.getItem('pzdiffsort') || 'sim';
  607. const initBtn = {
  608. sim: simBtn,
  609. time: timeBtn,
  610. default: defaultBtn,
  611. };
  612. document.querySelector('.translation-memory .col-auto').after(initBtn[sortType]);
  613. }
  614. // #endregion
  615.  
  616. // #region 初始化自动编辑 initAuto
  617. async function initAuto() {
  618. const avatars = await waitForElems('.nav-item.user-info');
  619. avatars.forEach(async (avatar) => {
  620. let harvesting = false;
  621. let translationPattern, skipPattern, userTime;
  622. avatar.insertAdjacentHTML('afterend', `<li class="nav-item"><a href="javascript:;" target="_self" class="PZpp nav-link" role="button">PP收割机</a></li>`);
  623. document.querySelectorAll('.PZpp').forEach(btn => btn.addEventListener('click', async (e) => {
  624. if (location.pathname.split('/')[3] !== 'strings') return;
  625. harvesting = !harvesting;
  626. if (harvesting) {
  627. e.target.style.color = '#dc3545';
  628. translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码:
  629. original(原文)
  630. oldTrans(现有译文)
  631. suggest(第1条翻译建议)
  632. suggestSim(上者匹配度,最大100)`, 'original');
  633. if (translationPattern === null) return cancel();
  634. skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码:
  635. original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签)
  636. oldTrans(现有译文)
  637. suggest(第1条翻译建议)
  638. suggestSim(上者匹配度,最大100
  639. context(上下文内容)`, '');
  640. if (skipPattern === null) return cancel();
  641. if (skipPattern === '') skipPattern = 'false';
  642. userTime = prompt('请确认生成译文后等待时间(单位:ms)', '500');
  643. if (userTime === null) return cancel();
  644. function cancel() {
  645. harvesting = false;
  646. e.target.style.color = '';
  647. }
  648. } else {
  649. e.target.style.color = '';
  650. return;
  651. }
  652.  
  653. const hideAlert = document.createElement('style');
  654. document.head.appendChild(hideAlert);
  655. hideAlert.innerHTML = '.alert-success.alert-global{display:none}';
  656.  
  657. const checkboxs = [...document.querySelectorAll('.right .custom-checkbox')].slice(0, 2);
  658. const checkboxValues = checkboxs.map(e => e.__vue__.$data.localChecked);
  659. checkboxs.forEach(e => e.__vue__.$data.localChecked = true);
  660.  
  661. const print = {
  662. waiting: () => console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  663. skip: () => console.log('%cSKIP', 'background: #FFC107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  664. click: () => console.log('%cCLICK', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  665. end: () => console.log('%cTHE END', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  666. }
  667.  
  668. const INTERVAL = 100;
  669. let interval = INTERVAL;
  670. let lastInfo = null;
  671.  
  672. function prepareWait() {
  673. print.waiting();
  674. interval = INTERVAL;
  675. lastInfo = null;
  676. return true;
  677. }
  678.  
  679. function skipOrFin(originElem, nextButton) {
  680. if (nextString(nextButton)) return false;
  681. print.skip();
  682. interval = 50;
  683. lastInfo = [
  684. originElem,
  685. location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1
  686. ];
  687. return true;
  688. }
  689.  
  690. function nextString(button) {
  691. if (button.disabled) {
  692. print.end();
  693. harvesting = false;
  694. e.target.style.color = '';
  695. return true;
  696. }
  697. button.click();
  698. return false;
  699. }
  700.  
  701. try {
  702. while (true) {
  703. await sleep(interval);
  704.  
  705. if (lastInfo) {
  706. const [ lastOrigin, lastPage ] = lastInfo;
  707. // 已点击翻页,但原文未发生改变
  708. const skipWaiting = (location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1) !== lastPage
  709. && document.querySelector('.editor-core .original') === lastOrigin;
  710. if (skipWaiting && prepareWait()) continue;
  711. }
  712.  
  713. const originElem = document.querySelector('.editor-core .original');
  714. if (!originElem && prepareWait()) continue;
  715. const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1];
  716. if (!nextButton && prepareWait()) continue;
  717.  
  718. const original = originElem.textContent;
  719. const oldTrans = document.querySelector('textarea.translation').value;
  720. let suggest = null, suggestSim = 0;
  721. if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) {
  722. const suggestEle = (await waitForElems('.translation-memory .string-item .translation, .empty-sign'))[0];
  723. if (suggestEle.classList.contains('empty-sign')) {
  724. if (skipOrFin(originElem, nextButton)) continue; else break;
  725. }
  726. suggest = suggestEle.textContent;
  727. suggestSim = +suggestEle.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1);
  728. if ((translationPattern.includes('suggestSim') || skipPattern.includes('suggestSim')) && isNaN(suggestSim)) {
  729. if (skipOrFin(originElem, nextButton)) continue; else break;
  730. }
  731. }
  732. const context = document.querySelector('.context')?.textContent;
  733.  
  734. if (eval(skipPattern)) {
  735. if (skipOrFin(originElem, nextButton)) continue; else break;
  736. }
  737.  
  738. const translation = eval(translationPattern);
  739. if (!translation && prepareWait()) continue;
  740.  
  741. await mockInput(translation);
  742. await sleep(userTime);
  743. if (!harvesting) break; // 放在等待后,以便在等待间隔点击取消
  744.  
  745. const translateButton = document.querySelector('.right .btn-primary');
  746. if (!translateButton) {
  747. if (skipOrFin(originElem, nextButton)) continue; else break;
  748. } else {
  749. translateButton.click();
  750. print.click();
  751. interval = INTERVAL;
  752. lastInfo = null;
  753. continue;
  754. }
  755. }
  756. } catch (e) {
  757. console.error(e);
  758. alert('出错了!');
  759. } finally {
  760. hideAlert.remove();
  761. checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] });
  762. }
  763.  
  764. }));
  765. });
  766. }
  767. // #endregion
  768.  
  769. // #endregion
  770.  
  771. // #region 函数调用逻辑
  772.  
  773. addHotkeys();
  774. initAuto();
  775.  
  776. let stringPageTurned = true;
  777. async function actByPath(path) {
  778. if (path.split('/').pop() === 'strings') {
  779.  
  780. let original;
  781. let lastOriginHTML = '';
  782. let toObserve = document.body;
  783.  
  784. const observer = new MutationObserver((mutations) => {
  785.  
  786. fixAdvSch();
  787. if (shouldSkip()) return;
  788.  
  789. original = document.querySelector('.editor-core .original');
  790. if (!original) return;
  791. const originUpded = original.innerHTML !== lastOriginHTML;
  792. lastOriginHTML = original.innerHTML;
  793.  
  794. observer.disconnect();
  795. initDiffClick();
  796. extractDiff();
  797.  
  798. const markAll = () => {
  799. fixOrigin(original);
  800. removeContextTags();
  801. markSearchParams();
  802. markContext(original.textContent);
  803. };
  804.  
  805. if (stringPageTurned) {
  806. if (!originUpded) {
  807. connectObserve();
  808. return;
  809. }
  810. console.debug('framework loaded');
  811. initDropMark();
  812. initMarkJS();
  813. tweakButtons();
  814. addCopySearchBtn();
  815. addReSortBtn();
  816. watchContextBtn();
  817. markAll();
  818. stringPageTurned = false;
  819. connectObserve();
  820. return;
  821. }
  822.  
  823. if (originUpded) {
  824. console.debug('origin upded');
  825. cancelSearchResult();
  826. markAll();
  827. }
  828.  
  829. for (const mutation of mutations) {
  830. const { addedNodes, removedNodes } = mutation;
  831. // console.debug({ addedNodes, removedNodes });
  832. if (addedNodes.length === 1) {
  833. const node = addedNodes[0];
  834. if (node.matches?.('.list-group.tags')) {
  835. updFixedTags();
  836. continue;
  837. }
  838. if (node.matches?.('.string-item a.small')) {
  839. node.remove();
  840. continue;
  841. }
  842. if (node.matches?.('.modal-backdrop')) {
  843. autoSaveAll();
  844. continue;
  845. }
  846. } else if (removedNodes.length === 1) {
  847. const node = removedNodes[0];
  848. if (mutation.target.classList?.contains('translation-memory')
  849. && node.classList?.contains('loading')) {
  850. console.debug('suggests loaded');
  851. const suggests = document.querySelectorAll('.string-item');
  852. findTextWithin(suggests, original.textContent);
  853. getDefaultSorted(suggests);
  854. initSearchResultDiff(original.textContent);
  855. autoFill100(suggests, original.textContent);
  856. reSortSuggestsByMem(suggests);
  857. continue;
  858. }
  859. if (node.matches?.('.list-group.tags')) tagSelectController.abort();
  860. }
  861. }
  862.  
  863. connectObserve();
  864. });
  865.  
  866. connectObserve();
  867. function connectObserve() {
  868. observer.observe(toObserve, {
  869. childList: true,
  870. subtree: true,
  871. });
  872. }
  873.  
  874. return observer;
  875.  
  876. } else if (path.split('/').at(-2) === 'issues') {
  877. waitForElems('.text-content p img').then((imgs) => imgs.forEach(mediumZoom));
  878. } else if (path.split('/').pop() === 'history') {
  879.  
  880. let observer = new MutationObserver(() => {
  881. observer.disconnect();
  882. extractDiff();
  883. connectObserve();
  884. });
  885.  
  886. connectObserve();
  887. function connectObserve() {
  888. observer.observe(document.body, {
  889. childList: true,
  890. subtree: true,
  891. });
  892. }
  893.  
  894. return observer;
  895.  
  896. }
  897. }
  898. let cancelAct = await actByPath(location.pathname);
  899. (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async (to, from) => {
  900. dropLastTextareaMark?.();
  901. if (JSON.stringify(to.query) !== JSON.stringify(from.query)) {
  902. console.debug('query changed');
  903. if (to.path.split('/').pop() === 'strings') {
  904. stringPageTurned = true;
  905. }
  906. }
  907. if (to.path === from.path) return;
  908. tagSelectController.abort();
  909. cancelAct?.disconnect();
  910. console.debug('path changed');
  911. cancelAct = await actByPath(to.path);
  912. });
  913.  
  914. // #endregion
  915.  
  916. // #region 通用工具函数
  917. function waitForElems(selector) {
  918. return new Promise(resolve => {
  919. if (document.querySelector(selector)) {
  920. return resolve(document.querySelectorAll(selector));
  921. }
  922.  
  923. const observer = new MutationObserver(() => {
  924. if (document.querySelector(selector)) {
  925. resolve(document.querySelectorAll(selector));
  926. observer.disconnect();
  927. }
  928. });
  929.  
  930. observer.observe(document.body, {
  931. childList: true,
  932. subtree: true
  933. });
  934. });
  935. }
  936.  
  937. function sleep(delay) {
  938. return new Promise((resolve) => setTimeout(resolve, delay));
  939. }
  940.  
  941. function mockInput(text) {
  942. return new Promise((resolve) => {
  943. const textarea = document.querySelector('textarea.translation');
  944. if (!textarea) return;
  945. textarea.value = text;
  946. textarea.dispatchEvent(new Event('input', {
  947. bubbles: true,
  948. cancelable: true,
  949. }));
  950. return resolve(0);
  951. })
  952. }
  953.  
  954. function mockInsert(text) {
  955. const textarea = document.querySelector('textarea.translation');
  956. if (!textarea) return;
  957. const startPos = textarea.selectionStart;
  958. const endPos = textarea.selectionEnd;
  959. const currentText = textarea.value;
  960.  
  961. const before = currentText.slice(0, startPos);
  962. const after = currentText.slice(endPos);
  963.  
  964. mockInput(before + text + after);
  965.  
  966. textarea.selectionStart = startPos + text.length;
  967. textarea.selectionEnd = endPos + text.length;
  968. }
  969.  
  970. function debounce(func, timeout = 300) {
  971. let called = false;
  972. return (...args) => {
  973. if (!called) {
  974. func.apply(this, args);
  975. called = true;
  976. setTimeout(() => called = false, timeout);
  977. }
  978. };
  979. }
  980.  
  981. function normalizeString(str) {
  982. if (!str) return '';
  983. return str
  984. .replace(/[,.;'"-]/g, '')
  985. .replace(/\s+/g, '')
  986. .toLowerCase();
  987. }
  988. // #endregion
  989.  
  990. })();