(handlerName: K, f: T[K]) {
const { port } = this;
port.onMessage.addListener(msg => {
- if (msg.name === handlerName) {
+ if (isMessage(msg) && 'name' in msg && msg.name === handlerName) {
const res = f(msg.msg);
port.postMessage({ msg: res, id: msg.id });
}
});
}
}
-
-Object.assign(globalThis, { MessageManager });
-
-/** @type {typeof MessageManager} MessageManager */
diff --git a/src/background_scripts/index.ts b/src/background_scripts/index.ts
new file mode 100644
index 0000000..632f9a5
--- /dev/null
+++ b/src/background_scripts/index.ts
@@ -0,0 +1,28 @@
+import browser from 'webextension-polyfill';
+import { getJyutpingList } from 'to-jyutping';
+
+import MessageManager from '../MessageManager';
+
+/* Communicate with content script */
+
+browser.runtime.onConnect.addListener(port => {
+ const mm: MessageManager<{ convert(msg: string): [string, string | null][] }> = new MessageManager(port);
+ mm.registerHandler('convert', getJyutpingList);
+});
+
+/* Context Menu */
+
+browser.contextMenus.onClicked.addListener((info, tab) => {
+ if (info.menuItemId === 'do-inject-jyutping') {
+ browser.tabs.sendMessage(tab?.id || 0, { name: 'do-inject-jyutping' });
+ }
+});
+
+(async () => {
+ await browser.contextMenus.removeAll();
+ browser.contextMenus.create({
+ id: 'do-inject-jyutping',
+ title: browser.i18n.getMessage('contextMenuItemDoInjectJyutping'),
+ contexts: ['page'],
+ });
+})();
diff --git a/content_scripts/index.css b/src/content_scripts/index.css
similarity index 72%
rename from content_scripts/index.css
rename to src/content_scripts/index.css
index 72754ba..c4ddd31 100644
--- a/content_scripts/index.css
+++ b/src/content_scripts/index.css
@@ -6,3 +6,7 @@ ruby.inject-jyutping > rt {
text-transform: initial;
letter-spacing: initial;
}
+
+ruby.inject-jyutping > rt::after {
+ content: attr(data-content);
+}
diff --git a/content_scripts/index.js b/src/content_scripts/index.ts
similarity index 58%
rename from content_scripts/index.js
rename to src/content_scripts/index.ts
index 4d295fa..83fb68c 100644
--- a/content_scripts/index.js
+++ b/src/content_scripts/index.ts
@@ -1,68 +1,60 @@
+import browser from 'webextension-polyfill';
+
+import MessageManager from '../MessageManager';
+import './index.css';
+
/**
* Check if a string contains Chinese characters.
- * @param {string} s The string to be checked
- * @return {boolean} Whether the string contains at least one Chinese character.
+ * @param s The string to be checked
+ * @return Whether the string contains at least one Chinese character.
*/
-function hasHanChar(s) {
+function hasHanChar(s: string): boolean {
return /[\p{Unified_Ideograph}\u3006\u3007]/u.test(s);
}
/**
* Determine whether an HTML element should be handled by inject-jyutping
* by checking its lang tag.
- * @param {string} lang The lang tag of an HTML element
- * @return {boolean} If the lang tag is reasonable to be handled, returns
+ * @param lang The lang tag of an HTML element
+ * @return If the lang tag is reasonable to be handled, returns
* true. Otherwise returns false.
*/
-function isTargetLang(lang) {
- return !lang.startsWith('ja') && !lang.startsWith('ko') && !lang.startsWith('vi');
+function isTargetLang(locale: string): boolean {
+ const [lang] = locale.split('-', 1);
+ return lang !== 'ja' && lang !== 'ko' && lang !== 'vi';
}
/**
* Create a ruby element with the character and the pronunciation.
- * @param {string} ch The character in a ruby element
- * @param {string} pronunciation The pronunciation in a ruby element
- * @return {Element} The ruby element
+ * @param ch The character in a ruby element
+ * @param pronunciation The pronunciation in a ruby element
+ * @return The ruby element
*/
-function makeRuby(ch, pronunciation) {
+function makeRuby(ch: string, pronunciation: string): Element {
const ruby = document.createElement('ruby');
ruby.classList.add('inject-jyutping');
- ruby.innerText = ch;
-
- const rp_left = document.createElement('rp');
- rp_left.appendChild(document.createTextNode('('));
- ruby.appendChild(rp_left);
+ ruby.textContent = ch;
const rt = document.createElement('rt');
rt.lang = 'yue-Latn';
- rt.innerText = pronunciation;
+ rt.dataset['content'] = pronunciation;
ruby.appendChild(rt);
- const rp_right = document.createElement('rp');
- rp_right.appendChild(document.createTextNode(')'));
- ruby.appendChild(rp_right);
-
return ruby;
}
const port = browser.runtime.connect();
-/** @type { MessageManager<{ convert(msg: string): [string, string | null][] }> } */
-const mm = new MessageManager(port);
+const mm: MessageManager<{ convert(msg: string): [string, string | null][] }> = new MessageManager(port);
const 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, /** @type {HTMLElement} */ (/** @type {Element} */ (element)?.closest?.('[lang]'))?.lang);
+ forEachText(node, convertText, ((element as Element)?.closest?.('[lang]') as HTMLElement)?.lang);
}
}
});
-/**
- * @param {Node} node
- * @param {(node: Node) => void} callback
- * @param {string} [lang = '']
- */
-function forEachText(node, callback, lang = '') {
+function forEachText(node: Node, callback: (node: Node) => void, lang = '') {
if (!isTargetLang(lang)) {
return;
}
@@ -76,29 +68,23 @@ function forEachText(node, callback, lang = '') {
return;
}
for (const child of node.childNodes) {
- forEachText(child, callback, /** @type {HTMLElement} */ (node).lang);
+ forEachText(child, callback, (node as HTMLElement).lang);
}
}
}
-/**
- * @param {Node} node
- */
-async function convertText(node) {
+async function convertText(node: 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.nodeValue !== newNodes.textContent) {
+ if (node.isConnected) {
node.parentNode?.replaceChild(newNodes, node);
}
}
-/**
- * @param {() => void} fn
- */
-function once(fn) {
+function once(fn: () => void) {
let called = false;
return () => {
if (called) return;
@@ -118,9 +104,10 @@ const init = once(() => {
});
browser.runtime.onMessage.addListener(msg => {
- if (msg.name === 'do-inject-jyutping') {
+ if (typeof msg === 'object' && msg && 'name' in msg && msg.name === 'do-inject-jyutping') {
init();
}
+ return undefined;
});
async function autoInit() {
diff --git a/popup/index.css b/src/popup/index.css
similarity index 100%
rename from popup/index.css
rename to src/popup/index.css
diff --git a/popup/index.html b/src/popup/index.html
similarity index 90%
rename from popup/index.html
rename to src/popup/index.html
index f178eef..5d5d721 100644
--- a/popup/index.html
+++ b/src/popup/index.html
@@ -2,8 +2,7 @@
-
-
+
diff --git a/popup/index.js b/src/popup/index.ts
similarity index 57%
rename from popup/index.js
rename to src/popup/index.ts
index 78c0cac..5e4f7ad 100644
--- a/popup/index.js
+++ b/src/popup/index.ts
@@ -1,10 +1,12 @@
-import './lib/webextension-polyfill.js';
+import browser from 'webextension-polyfill';
+
+import './index.css';
const i = browser.i18n.getMessage;
-const autoInjectNativeText = /** @type {HTMLDivElement} */ (document.getElementById('auto-inject-native-text'));
-const autoInjectCheckbox = /** @type {HTMLInputElement} */ (document.getElementById('auto-inject-checkbox'));
-const refreshPromptText = /** @type {HTMLParagraphElement} */ (document.getElementById('refresh-prompt-text'));
+const autoInjectNativeText = document.getElementById('auto-inject-native-text') as HTMLDivElement;
+const autoInjectCheckbox = document.getElementById('auto-inject-checkbox') as HTMLInputElement;
+const refreshPromptText = document.getElementById('refresh-prompt-text') as HTMLParagraphElement;
/* Initialize state */
(async () => {
diff --git a/jsconfig.json b/tsconfig.json
similarity index 77%
rename from jsconfig.json
rename to tsconfig.json
index 2f25b32..2097f1e 100644
--- a/jsconfig.json
+++ b/tsconfig.json
@@ -2,8 +2,9 @@
"compilerOptions": {
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "moduleDetection": "force",
"strict": true,
"allowUnusedLabels": false,
@@ -15,7 +16,6 @@
"isolatedModules": true,
"verbatimModuleSyntax": true,
- "checkJs": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"useDefineForClassFields": true,
@@ -23,5 +23,5 @@
"incremental": true,
"noEmit": true
},
- "exclude": ["node_modules", "./lib/webextension-polyfill.js", "./lib/to-jyutping.js"]
+ "include": ["src"]
}