Han Simplify

将页面上的汉字转换为简体字,需要手动添加包含的网站以启用

当前为 2022-03-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Han Simplify
  3. // @name:zh 汉字转换为简体字
  4. // @description 将页面上的汉字转换为简体字,需要手动添加包含的网站以启用
  5. // @namespace https://github.com/tiansh
  6. // @version 1.0
  7. // @resource t2s https://tiansh.github.io/reader/data/han/t2s.json
  8. // @include *
  9. // @exclude *
  10. // @grant GM_getResourceURL
  11. // @grant GM.getResourceUrl
  12. // @run-at document-start
  13. // @license MIT
  14. // @supportURL https://github.com/tiansh/us-han-simplify/issues
  15. // ==/UserScript==
  16.  
  17. /* eslint-env browser, greasemonkey */
  18.  
  19. ; (async function () {
  20. /** @type {'t2s'|'s2t'} */
  21. const RULE = 't2s';
  22.  
  23. const fetchTable = async function (url) {
  24. return (await fetch(url)).json();
  25. };
  26. const loadTable = async function () {
  27. try {
  28. return fetchTable(GM_getResourceURL(RULE));
  29. } catch {
  30. return fetchTable(await GM.getResourceUrl(RULE));
  31. }
  32. };
  33.  
  34. /** @type {{ [ch: string]: [string, number] }[]} */
  35. const table = await loadTable();
  36. /** @param {string} text */
  37. const translate = function (text) {
  38. let output = '';
  39. let state = 0;
  40. const hasOwnProperty = Object.prototype.hasOwnProperty;
  41. for (let char of text) {
  42. while (true) {
  43. const current = table[state];
  44. const hasMatch = hasOwnProperty.call(current, char);
  45. if (!hasMatch && state === 0) {
  46. output += char;
  47. break;
  48. }
  49. if (hasMatch) {
  50. const [adding, next] = current[char];
  51. if (adding) output += adding;
  52. state = next;
  53. break;
  54. }
  55. const [adding, next] = current[''];
  56. if (adding) output += adding;
  57. state = next;
  58. }
  59. }
  60. while (state !== 0) {
  61. const current = table[state];
  62. const [adding, next] = current[''];
  63. if (adding) output += adding;
  64. state = next;
  65. }
  66. return output;
  67. };
  68.  
  69. const correctLangTags = function () {
  70. [...document.querySelectorAll('[lang]:not([hanconv-lang])')].forEach(element => {
  71. const lang = element.getAttribute('lang');
  72. element.setAttribute('hanconv-lang', lang);
  73. if (RULE === 't2s' && /^zh\b(?:(?!.*-Hans)-(?:TW|HK|MO)|.*-Hant|$)/i.test(lang)) {
  74. element.setAttribute('lang', 'zh-Hans');
  75. }
  76. if (RULE === 's2t' && /^zh\b(?:(?!.*-Hant)-(?:CN|SG|MY)|.*-Hans|$)/i.test(lang)) {
  77. element.setAttribute('lang', 'zh-Hant');
  78. }
  79. if (!/^(?:ja|ko|vi)\b/i.test(lang)) {
  80. element.setAttribute('hanconv-apply', 'apply');
  81. }
  82. });
  83. };
  84.  
  85. /** @type {WeakMap<Text|Attr, string>} */
  86. const translated = new WeakMap();
  87. /** @param {Element} element */
  88. const needTranslateElement = function (element) {
  89. if (element.matches('script, style')) return false;
  90. if (element.closest('svg, math, .notranslate, [translate="no"], code:not([translate="yes"]), var:not([translate="yes"])')) return false;
  91. const lang = element.closest('[lang]');
  92. return lang == null || lang.hasAttribute('hanconv-apply');
  93. };
  94. /** @param {Text|Attr} node */
  95. const needTranslate = function (node) {
  96. if (translated.has(node) && translated.get(node) === node.nodeValue) return false;
  97. if (/^\s*$/.test(node.nodeValue)) return false;
  98. const element = node instanceof Text ? node.parentElement : node.ownerElement;
  99. if (!element) return true;
  100. return needTranslateElement(element);
  101. };
  102. /** @param {Text|Attr} node */
  103. const translateNode = function (node) {
  104. if (!needTranslate(node)) return;
  105. const result = translate(node.nodeValue);
  106. translated.set(node, result);
  107. node.nodeValue = result;
  108. };
  109. const translateContainer = function (container) {
  110. if (container instanceof Text || container instanceof Attr) {
  111. translateNode(container);
  112. } else if (container instanceof Element) {
  113. const nodes = document.evaluate([
  114. // Translate text
  115. '//text()',
  116. // Translate attributes
  117. '(//applet|//area|//img|//input)/@alt',
  118. '(//a|//area)/@download',
  119. '//@title',
  120. '//@aria-label', '//@aria-description',
  121. ].join('|'), container, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  122. for (let index = 0; index < nodes.snapshotLength; index++) {
  123. translateNode(nodes.snapshotItem(index));
  124. }
  125. }
  126. };
  127.  
  128. let timeout = null;
  129. const updateInterval = 200;
  130. const translateTargets = new Set();
  131. const observer = new MutationObserver(function onMutate(records) {
  132. correctLangTags();
  133. records.forEach(record => {
  134. if (record.type === 'childList') {
  135. [...record.addedNodes].forEach(node => translateTargets.add(node));
  136. } else {
  137. translateTargets.add(record.target);
  138. }
  139. });
  140. if (!timeout) {
  141. const targets = [...translateTargets].filter((node, _, arr) => !arr.some(other => other !== node && other.contains(node)));
  142. translateTargets.clear();
  143. targets.forEach(translateContainer);
  144. timeout = setTimeout(() => { timeout = null; onMutate([]); }, updateInterval);
  145. }
  146. });
  147. observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
  148.  
  149. }());