注入粤拼

为汉字标注粤语发音(粤拼)。

  1. // ==UserScript==
  2. // @name Inject Jyutping
  3. // @name:zh-HK 注入粵拼
  4. // @name:zh-TW 注入粵拼
  5. // @name:zh-CN 注入粤拼
  6. // @name:ja 粤拼を注入
  7. // @name:ko 월병(粵拼)을 주입
  8. // @description Add Cantonese pronunciation (Jyutping) on Chinese characters.
  9. // @description:zh-HK 為漢字標註粵語發音(粵拼)。
  10. // @description:zh-TW 為漢字標註粵語發音(粵拼)。
  11. // @description:zh-CN 为汉字标注粤语发音(粤拼)。
  12. // @description:ja 漢字に広東語の発音(粤拼)を付けます。
  13. // @description:ko 한자에 광동어의 발음(월병/粵拼)을 붙인다.
  14. // @namespace https://jyutping.org
  15. // @version 0.5.0
  16. // @license BSD-2-Clause
  17. // @icon https://raw.githubusercontent.com/CanCLID/inject-jyutping/refs/heads/main/icons/128.png
  18. // @match *://*/*
  19. // @grant GM_addStyle
  20. // @run-at context-menu
  21. // ==/UserScript==
  22.  
  23. 'use strict';
  24.  
  25. GM_addStyle(`
  26. ruby.inject-jyutping > rt {
  27. font-size: 0.74em;
  28. font-variant: initial;
  29. margin-left: 0.1em;
  30. margin-right: 0.1em;
  31. text-transform: initial;
  32. letter-spacing: initial;
  33. }
  34.  
  35. ruby.inject-jyutping > rt::after {
  36. content: attr(data-content);
  37. }
  38. `);
  39.  
  40. // src/MessageManager.ts
  41. function isMessage(obj) {
  42. return obj && typeof obj === "object" && typeof obj.id === "number" && "msg" in obj;
  43. }
  44. var getUniqueId = ((id) => () => id++)(0);
  45.  
  46. class MessageManager {
  47. worker;
  48. constructor(worker) {
  49. this.worker = worker;
  50. }
  51. sendMessage(handlerName, msg) {
  52. const { worker } = this;
  53. const id = getUniqueId();
  54. return new Promise((resolve) => {
  55. worker.addEventListener("message", function f({ data: response }) {
  56. if (isMessage(response) && response.id === id) {
  57. worker.removeEventListener("message", f);
  58. resolve(response.msg);
  59. }
  60. });
  61. worker.postMessage({ msg, id, name: handlerName });
  62. });
  63. }
  64. registerHandler(handlerName, f) {
  65. const { worker } = this;
  66. worker.addEventListener("message", ({ data: msg }) => {
  67. if (isMessage(msg) && "name" in msg && msg.name === handlerName) {
  68. const res = f(msg.msg);
  69. worker.postMessage({ msg: res, id: msg.id });
  70. }
  71. });
  72. }
  73. }
  74.  
  75. // src/index.ts
  76. function hasHanChar(s) {
  77. return /[\p{Unified_Ideograph}\u3006\u3007]/u.test(s);
  78. }
  79. function isTargetLang(locale) {
  80. const [lang] = locale.split("-", 1);
  81. return lang !== "ja" && lang !== "ko" && lang !== "vi";
  82. }
  83. function makeRuby(ch, pronunciation) {
  84. const ruby = document.createElement("ruby");
  85. ruby.classList.add("inject-jyutping");
  86. ruby.textContent = ch;
  87. const rt = document.createElement("rt");
  88. rt.lang = "yue-Latn";
  89. rt.dataset["content"] = pronunciation;
  90. ruby.appendChild(rt);
  91. return ruby;
  92. }
  93. function forEachText(node, callback, lang = "") {
  94. if (!isTargetLang(lang)) {
  95. return;
  96. }
  97. if (node.nodeType === Node.TEXT_NODE) {
  98. if (hasHanChar(node.nodeValue || "")) {
  99. callback(node);
  100. }
  101. } else if (node.nodeType === Node.ELEMENT_NODE) {
  102. if (["RUBY", "OPTION", "TEXTAREA", "SCRIPT", "STYLE"].includes(node.nodeName)) {
  103. return;
  104. }
  105. for (const child of node.childNodes) {
  106. forEachText(child, callback, node.lang);
  107. }
  108. }
  109. }
  110. async function convertText(node) {
  111. const conversionResults = await mm.sendMessage("convert", node.nodeValue || "");
  112. const newNodes = document.createDocumentFragment();
  113. for (const [k, v] of conversionResults) {
  114. newNodes.appendChild(v === null ? document.createTextNode(k) : makeRuby(k, v));
  115. }
  116. if (node.isConnected) {
  117. node.parentNode?.replaceChild(newNodes, node);
  118. }
  119. }
  120. var worker = new Worker(URL.createObjectURL(new Blob(["function o(e){return e&&typeof e===\"object\"&&typeof e.id===\"number\"&&\"msg\"in e}var d=((e)=>()=>e++)(0);class n{e;constructor(e){this.worker=e}sendMessage(e,r){const{worker:s}=this,t=d();return new Promise((i)=>{s.addEventListener(\"message\",function g({data:a}){if(o(a)&&a.id===t)s.removeEventListener(\"message\",g),i(a.msg)}),s.postMessage({msg:r,id:t,name:e})})}registerHandler(e,r){const{worker:s}=this;s.addEventListener(\"message\",({data:t})=>{if(o(t)&&\"name\"in t&&t.name===e){const i=r(t.msg);s.postMessage({msg:i,id:t.id})}})}}importScripts(\"https://cdn.jsdelivr.net/npm/to-jyutping@3.1.1\");var c=new n(globalThis);c.registerHandler(\"convert\",ToJyutping.getJyutpingList);\n"], { type: "text/javascript" })));
  121. var mm = new MessageManager(worker);
  122. var mo = new MutationObserver((changes) => {
  123. for (const change of changes) {
  124. for (const node of change.addedNodes) {
  125. const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentNode;
  126. forEachText(node, convertText, element?.closest?.("[lang]")?.lang);
  127. }
  128. }
  129. });
  130. forEachText(document.body, convertText, document.body.lang || document.documentElement.lang);
  131. mo.observe(document.body, {
  132. characterData: true,
  133. childList: true,
  134. subtree: true
  135. });