-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
121 lines (102 loc) · 3.9 KB
/
Copy pathcontent.js
File metadata and controls
121 lines (102 loc) · 3.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class FuriganaInjector {
constructor() {
this.busy = false;
this.skipTags = new Set([
"SCRIPT", "STYLE", "CODE", "PRE", "TEXTAREA",
"RUBY", "RT", "RP", "NOSCRIPT", "HEAD", "TITLE"
]);
this.hasKanji = /[\u4e00-\u9fff]/;
}
async annotate() {
if (this.busy) return;
this.busy = true;
try {
// Wait a tick to ensure page has fully rendered
await new Promise(requestAnimationFrame);
const nodes = this.collectTextNodes(document.body);
console.log("[furigana] annotating", nodes.length, "nodes");
console.time("[furigana] total");
await this.annotateNodes(nodes);
console.timeEnd("[furigana] total");
console.log("[furigana] done");
} finally {
this.busy = false;
}
}
async annotateSelection() {
if (this.busy) return;
this.busy = true;
try {
await new Promise(requestAnimationFrame);
const selection = window.getSelection();
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const nodes = this.collectTextNodes(range.commonAncestorContainer, range);
console.log("[furigana] annotating", nodes.length, "selected nodes");
console.time("[furigana] selection total");
await this.annotateNodes(nodes);
console.timeEnd("[furigana] selection total");
console.log("[furigana] selection done");
} finally {
this.busy = false;
}
}
async annotateNodes(nodes) {
// Split nodes into batches
const chunkSize = 100;
const chunks = [];
for (let i = 0; i < nodes.length; i += chunkSize) {
chunks.push(nodes.slice(i, i + chunkSize));
}
// Send all batches to background for annotation
const promises = chunks.map(chunk =>
browser.runtime.sendMessage({
type: "ANNOTATE_BATCH",
texts: chunk.map(n => n.textContent)
})
);
// Apply results as they come back, replacing original text with annotated HTML
const range = document.createRange();
for (let i = 0; i < promises.length; i++) {
const results = await promises[i];
chunks[i].forEach((node, j) => {
if (results[j] === null) return;
node.replaceWith(range.createContextualFragment(results[j]));
});
}
}
collectTextNodes(root, range = null) {
const treeRoot = root.nodeType === Node.TEXT_NODE ? root.parentNode : root;
const nodes = [];
const walker = document.createTreeWalker(
treeRoot,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (this.skipTags.has(node.tagName)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_SKIP;
}
if (!this.hasKanji.test(node.textContent)) return NodeFilter.FILTER_REJECT;
if (range && !range.intersectsNode(node)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while ((node = walker.nextNode())) nodes.push(node);
return nodes;
}
listen() {
browser.runtime.onMessage.addListener((message) => {
if (message.type === "ANNOTATE_PAGE") {
this.annotate();
}
if (message.type === "ANNOTATE_SELECTION") {
this.annotateSelection();
}
});
}
}
const injector = new FuriganaInjector();
injector.listen();