注入粤拼

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

当前为 2024-10-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              Inject Jyutping
// @name:zh-HK        注入粵拼
// @name:zh-TW        注入粵拼
// @name:zh-CN        注入粤拼
// @name:ja           粤拼を注入
// @name:ko           월병(粵拼)을 주입
// @description       Add Cantonese pronunciation (Jyutping) on Chinese characters.
// @description:zh-HK 為漢字標註粵語發音(粵拼)。
// @description:zh-TW 為漢字標註粵語發音(粵拼)。
// @description:zh-CN 为汉字标注粤语发音(粤拼)。
// @description:ja    漢字に広東語の発音(粤拼)を付けます。
// @description:ko    한자에 광동어의 발음(월병/粵拼)을 붙인다.
// @namespace         https://jyutping.org
// @version           0.5.0
// @license           BSD-2-Clause
// @icon              https://raw.githubusercontent.com/CanCLID/inject-jyutping/refs/heads/main/icons/128.png
// @match             *://*/*
// @grant             GM_addStyle
// @run-at            context-menu
// ==/UserScript==

'use strict';

GM_addStyle(`
    ruby.inject-jyutping > rt {
        font-size: 0.74em;
        font-variant: initial;
        margin-left: 0.1em;
        margin-right: 0.1em;
        text-transform: initial;
        letter-spacing: initial;
    }

    ruby.inject-jyutping > rt::after {
        content: attr(data-content);
    }
`);

// src/MessageManager.ts
function isMessage(obj) {
  return obj && typeof obj === "object" && typeof obj.id === "number" && "msg" in obj;
}
var getUniqueId = ((id) => () => id++)(0);

class MessageManager {
  worker;
  constructor(worker) {
    this.worker = worker;
  }
  sendMessage(handlerName, msg) {
    const { worker } = this;
    const id = getUniqueId();
    return new Promise((resolve) => {
      worker.addEventListener("message", function f({ data: response }) {
        if (isMessage(response) && response.id === id) {
          worker.removeEventListener("message", f);
          resolve(response.msg);
        }
      });
      worker.postMessage({ msg, id, name: handlerName });
    });
  }
  registerHandler(handlerName, f) {
    const { worker } = this;
    worker.addEventListener("message", ({ data: msg }) => {
      if (isMessage(msg) && "name" in msg && msg.name === handlerName) {
        const res = f(msg.msg);
        worker.postMessage({ msg: res, id: msg.id });
      }
    });
  }
}

// src/index.ts
function hasHanChar(s) {
  return /[\p{Unified_Ideograph}\u3006\u3007]/u.test(s);
}
function isTargetLang(locale) {
  const [lang] = locale.split("-", 1);
  return lang !== "ja" && lang !== "ko" && lang !== "vi";
}
function makeRuby(ch, pronunciation) {
  const ruby = document.createElement("ruby");
  ruby.classList.add("inject-jyutping");
  ruby.textContent = ch;
  const rt = document.createElement("rt");
  rt.lang = "yue-Latn";
  rt.dataset["content"] = pronunciation;
  ruby.appendChild(rt);
  return ruby;
}
function forEachText(node, callback, lang = "") {
  if (!isTargetLang(lang)) {
    return;
  }
  if (node.nodeType === Node.TEXT_NODE) {
    if (hasHanChar(node.nodeValue || "")) {
      callback(node);
    }
  } else if (node.nodeType === Node.ELEMENT_NODE) {
    if (["RUBY", "OPTION", "TEXTAREA", "SCRIPT", "STYLE"].includes(node.nodeName)) {
      return;
    }
    for (const child of node.childNodes) {
      forEachText(child, callback, node.lang);
    }
  }
}
async function convertText(node) {
  const conversionResults = await mm.sendMessage("convert", node.nodeValue || "");
  const newNodes = document.createDocumentFragment();
  for (const [k, v] of conversionResults) {
    newNodes.appendChild(v === null ? document.createTextNode(k) : makeRuby(k, v));
  }
  if (node.isConnected) {
    node.parentNode?.replaceChild(newNodes, node);
  }
}
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/[email protected]\");var c=new n(globalThis);c.registerHandler(\"convert\",ToJyutping.getJyutpingList);\n"], { type: "text/javascript" })));
var mm = new MessageManager(worker);
var mo = new MutationObserver((changes) => {
  for (const change of changes) {
    for (const node of change.addedNodes) {
      const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentNode;
      forEachText(node, convertText, element?.closest?.("[lang]")?.lang);
    }
  }
});
forEachText(document.body, convertText, document.body.lang || document.documentElement.lang);
mo.observe(document.body, {
  characterData: true,
  childList: true,
  subtree: true
});