ParaTranz diff

ParaTranz enhanced

目前为 2025-02-05 提交的版本。查看 最新版本

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