Han Simplify

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

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

  1. // ==UserScript==
  2. // @name Han Simplify
  3. // @name:zh 汉字转换为简体字
  4. // @description 将页面上的汉字转换为简体字,需要手动添加包含的网站以启用
  5. // @namespace https://github.com/tiansh
  6. // @version 1.6
  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. /** @type {'t2s'|'s2t'} */
  18. const RULE = 't2s';
  19.  
  20. /* global RULE */
  21. /**
  22. * @name RULE
  23. * @type {'t2s'|'s2t'}
  24. */
  25. /* eslint-env browser, greasemonkey */
  26.  
  27. ; (async function () {
  28.  
  29. const fetchTable = async function (url) {
  30. return (await fetch(url)).json();
  31. };
  32. const loadTable = async function () {
  33. try {
  34. return fetchTable(GM_getResourceURL(RULE));
  35. } catch {
  36. return fetchTable(await GM.getResourceUrl(RULE));
  37. }
  38. };
  39.  
  40. /** @type {{ [ch: string]: [string, number] }[]} */
  41. const table = await loadTable();
  42. const hasOwnProperty = Object.prototype.hasOwnProperty;
  43. /** @param {string} text */
  44. const translate = function (text) {
  45. let output = '';
  46. let state = 0;
  47. for (let char of text) {
  48. while (true) {
  49. const current = table[state];
  50. const hasMatch = hasOwnProperty.call(current, char);
  51. if (!hasMatch && state === 0) {
  52. output += char;
  53. break;
  54. }
  55. if (hasMatch) {
  56. const [adding, next] = current[char];
  57. if (adding) output += adding;
  58. state = next;
  59. break;
  60. }
  61. const [adding, next] = current[''];
  62. if (adding) output += adding;
  63. state = next;
  64. }
  65. }
  66. while (state !== 0) {
  67. const current = table[state];
  68. const [adding, next] = current[''];
  69. if (adding) output += adding;
  70. state = next;
  71. }
  72. return output;
  73. };
  74.  
  75. const correctLangTags = function () {
  76. [...document.querySelectorAll('[lang]:not([hanconv-lang])')].forEach(element => {
  77. const lang = element.getAttribute('lang');
  78. element.setAttribute('hanconv-lang', lang);
  79. if (RULE === 't2s' && /^zh\b(?:(?!.*-Hans)-(?:TW|HK|MO)|.*-Hant|$)/i.test(lang)) {
  80. element.setAttribute('lang', 'zh-Hans');
  81. }
  82. if (RULE === 's2t' && /^zh\b(?:(?!.*-Hant)-(?:CN|SG|MY)|.*-Hans|$)/i.test(lang)) {
  83. element.setAttribute('lang', 'zh-Hant');
  84. }
  85. if (!/^(?:ja|ko|vi)\b/i.test(lang)) {
  86. element.setAttribute('hanconv-apply', 'apply');
  87. }
  88. });
  89. };
  90.  
  91. /** @type {WeakMap<Text|Attr, string>} */
  92. const translated = new WeakMap();
  93. /** @param {Element} element */
  94. const needTranslateElement = function (element) {
  95. if (element.matches('script, style')) return false;
  96. if (element.closest('svg, math, .notranslate, [translate="no"], code:not([translate="yes"]), var:not([translate="yes"]), [contenteditable="true"]')) return false;
  97. const lang = element.closest('[lang]');
  98. return lang == null || lang.hasAttribute('hanconv-apply');
  99. };
  100. /** @param {Text|Attr} node */
  101. const needTranslateNode = function (node) {
  102. if (translated.has(node) && translated.get(node) === node.nodeValue) return false;
  103. if (/^\s*$/.test(node.nodeValue)) return false;
  104. return true;
  105. };
  106. /** @param {Text|Attr} node */
  107. const translateNode = function (node) {
  108. if (!node || !needTranslateNode(node)) return;
  109. const result = translate(node.nodeValue);
  110. translated.set(node, result);
  111. node.nodeValue = result;
  112. };
  113. const translateTree = function translateTree(node) {
  114. if (node instanceof Text) {
  115. translateNode(node);
  116. } else if (node instanceof Element) {
  117. const tagName = node.tagName;
  118. if (node.attributes.lang && !node.attributes['hanconv-apply']) return;
  119. if (node.classList.contains('notranslate')) return;
  120. const contenteditable = node.getAttribute('contenteditable');
  121. if (contenteditable === 'true') return;
  122. const translate = node.getAttribute('translate');
  123. if (translate === 'no') return;
  124. if (['CODE', 'VAR'].includes(tagName) && translate !== 'yes') return;
  125.  
  126. const attrs = node.attributes;
  127. if (['APPLET', 'AREA', 'IMG', 'INPUT'].includes(tagName)) translateNode(attrs.alt);
  128. if (['INPUT', 'TEXTAREA'].includes(tagName)) translateNode(attrs.placeholder);
  129. if (['A', 'AREA'].includes(tagName)) translateNode(attrs.download);
  130. translateNode(attrs.title);
  131. translateNode(attrs['aria-label']);
  132. translateNode(attrs['aria-description']);
  133.  
  134. if (['SVG', 'MATH', 'SCRIPT', 'STYLE', 'TEXTAREA'].includes(tagName)) return;
  135. [...node.childNodes].forEach(translateTree);
  136. } else if (node instanceof Document) {
  137. [...node.childNodes].forEach(translateTree);
  138. }
  139. };
  140. /** @param {Element} container */
  141. const translateContainer = function (container) {
  142. if (container instanceof Text) {
  143. if (needTranslateElement(container.parentElement)) translateNode(container);
  144. } else if (container instanceof Attr) {
  145. if (needTranslateElement(container.ownerElement)) translateNode(container);
  146. } else if (container instanceof Element) {
  147. if (!needTranslateElement(container)) {
  148. [...container.querySelectorAll('[hanconv-apply]')].forEach(translateContainer);
  149. return;
  150. }
  151. translateTree(container);
  152. } else if (container instanceof Document) {
  153. translateTree(container);
  154. }
  155. };
  156.  
  157. const observer = new MutationObserver(function onMutate(records) {
  158. correctLangTags();
  159. const translateTargets = new Set();
  160. records.forEach(record => {
  161. if (record.type === 'childList') {
  162. [...record.addedNodes].forEach(node => translateTargets.add(node));
  163. } else {
  164. translateTargets.add(record.target);
  165. }
  166. });
  167. [...translateTargets].forEach(translateContainer);
  168. });
  169. observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
  170.  
  171. if (document.readyState === 'complete') {
  172. translateContainer(document);
  173. } else document.addEventListener('DOMContentLoaded', () => {
  174. translateContainer(document);
  175. }, { once: true });
  176.  
  177. }());