Han Simplify

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

  1. // ==UserScript==
  2. // @name Han Simplify
  3. // @name:zh 汉字转换为简体字
  4. // @description 将页面上的汉字转换为简体字,需要手动添加包含的网站以启用
  5. // @namespace https://github.com/tiansh
  6. // @version 1.7
  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. // Do not characters marked as following languages
  76. const skipLang = /^(?:ja|ko|vi)\b/i;
  77. // Change lang attribute so correct fonts may be available
  78. const fromLang = {
  79. t2s: /^zh\b(?:(?!.*-Hans)-(?:TW|HK|MO)|.*-Hant|$)/i,
  80. s2t: /^zh\b(?:(?!.*-Hant)-(?:CN|SG|MY)|.*-Hans|$)/i,
  81. }[RULE];
  82. // Overwrite language attribute with
  83. const destLang = { t2s: 'zh-Hans', s2t: 'zh-Hant' }[RULE];
  84.  
  85. /** @type {WeakMap<Text|Attr, string>} */
  86. const translated = new WeakMap();
  87. /** @param {Text|Attr} node */
  88. const translateNode = function (node) {
  89. if (!node) return;
  90. if (translated.has(node) && translated.get(node) === node.nodeValue) return;
  91. if (/^\s*$/.test(node.nodeValue)) return;
  92. const result = translate(node.nodeValue);
  93. translated.set(node, result);
  94. node.nodeValue = result;
  95. };
  96.  
  97. /** @enum {number} */
  98. const filterResult = {
  99. TRANSLATE: 0, // Translate this node
  100. SKIP_CHILD: 1, // Translate this node but not its children
  101. SKIP_LANG: 2, // Do not translate nodes in certain language
  102. SKIP_NODE: 3, // Do not translate this node and its children
  103. };
  104. /**
  105. * @param {Document|Element|Text} node
  106. * @param {filterResult} context
  107. */
  108. const nodeFilter = function (node, context = null) {
  109. // DOM Root
  110. if (node instanceof Document) return filterResult.TRANSLATE;
  111. // Inherit skip
  112. if (context === filterResult.SKIP_NODE || context === filterResult.SKIP_CHILD) return filterResult.SKIP_NODE;
  113. // Text Node
  114. if (node instanceof Text) return context === filterResult.TRANSLATE ? filterResult.TRANSLATE : filterResult.SKIP_NODE;
  115. // Skip other unknown nodes
  116. if (!(node instanceof Element)) return filterResult.SKIP_NODE;
  117. // Do not translate nodes which marked no translate
  118. if (node.classList.contains('notranslate')) return filterResult.SKIP_NODE;
  119. const translate = node.getAttribute('translate');
  120. if (translate === 'no') return filterResult.SKIP_NODE;
  121. // Do not translate content of certain type elements
  122. const tagName = node.tagName;
  123. let child = true;
  124. if (['CODE', 'VAR'].includes(tagName) && translate !== 'yes') child = false;
  125. else if (['SVG', 'MATH', 'SCRIPT', 'STYLE', 'TEXTAREA'].includes(tagName)) child = false;
  126. else if (node.getAttribute('contenteditable') === 'true') child = false;
  127. const lang = node.getAttribute('lang');
  128. // If no language is specified
  129. if (!lang) {
  130. if (child) return context;
  131. return context === filterResult.TRANSLATE ? filterResult.SKIP_CHILD : filterResult.SKIP_NODE;
  132. }
  133. // If text in languages that should be ignored
  134. if (skipLang.test(lang)) {
  135. if (child) return filterResult.SKIP_LANG;
  136. else return filterResult.SKIP_NODE;
  137. }
  138. if (fromLang.test(lang)) {
  139. node.setAttribute('ori-lang', node.getAttribute('lang'));
  140. node.setAttribute('lang', destLang);
  141. }
  142. if (child) return filterResult.TRANSLATE;
  143. else return filterResult.SKIP_CHILD;
  144. };
  145. /** @param {Text|Element} node */
  146. const nodeFilterParents = function (node) {
  147. const parents = [];
  148. for (let p = node; p; p = p.parentNode) parents.push(p);
  149. return parents.reverse().reduce((context, node) => nodeFilter(node, context), filterResult.TRANSLATE);
  150. };
  151. /**
  152. * @param {Node} node
  153. * @param {filterResult} context
  154. */
  155. const translateTree = function translateTree(node, context) {
  156. if (node instanceof Text) {
  157. if (context === filterResult.TRANSLATE) translateNode(node);
  158. } else if (node instanceof Element) {
  159. const filter = nodeFilter(node, context);
  160. if (filter === filterResult.SKIP_CHILD || filter === filterResult.TRANSLATE) {
  161. const tagName = node.tagName, attrs = node.attributes;
  162. if (['APPLET', 'AREA', 'IMG', 'INPUT'].includes(tagName)) translateNode(attrs.alt);
  163. if (['INPUT', 'TEXTAREA'].includes(tagName)) translateNode(attrs.placeholder);
  164. if (['A', 'AREA'].includes(tagName)) translateNode(attrs.download);
  165. translateNode(attrs.title);
  166. translateNode(attrs['aria-label']);
  167. translateNode(attrs['aria-description']);
  168. }
  169. if (filter === filterResult.TRANSLATE || filter === filterResult.SKIP_LANG) {
  170. [...node.childNodes].forEach(child => { translateTree(child, filter); });
  171. }
  172. } else if (node instanceof Document) {
  173. [...node.childNodes].forEach(child => { translateTree(child, filterResult.TRANSLATE); });
  174. }
  175. };
  176. /** @param {Text|Element} container */
  177. const translateContainer = function (container) {
  178. const filter = nodeFilterParents(container);
  179. if (filter !== filterResult.SKIP_NODE) translateTree(container, filter);
  180. };
  181.  
  182. const observer = new MutationObserver(function onMutate(records) {
  183. const translateTargets = new Set();
  184. records.forEach(record => {
  185. if (record.type === 'childList') {
  186. [...record.addedNodes].forEach(node => translateTargets.add(node));
  187. } else {
  188. translateTargets.add(record.target);
  189. }
  190. });
  191. [...translateTargets].forEach(translateContainer);
  192. });
  193. observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
  194.  
  195. if (document.readyState === 'complete') {
  196. translateContainer(document);
  197. } else document.addEventListener('DOMContentLoaded', () => {
  198. translateContainer(document);
  199. }, { once: true });
  200.  
  201. }());