@ingglish/dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +601 -0
- package/dist/index.mjs +572 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// src/translate/apply-map.ts
|
|
2
|
+
import { applyCasePattern as applyCasePattern2, detectCasePattern as detectCasePattern2, normalizeApostrophes as normalizeApostrophes3 } from "@ingglish/normalize";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var WORD_SPAN_CLASS = "ingglish-word";
|
|
6
|
+
var TOOLTIP_STYLES_ID = "ingglish-tooltip-styles";
|
|
7
|
+
var ATTR_ORIGINAL_WORD = "data-ingglish-orig";
|
|
8
|
+
var ATTR_ORIGINAL_CONTENT = "data-ingglish-original";
|
|
9
|
+
var ATTR_SKIP = "data-ingglish-skip";
|
|
10
|
+
var ATTR_ORIGINAL_PREFIX = "data-ingglish-original-";
|
|
11
|
+
var FORMAT_DIFF_CLASS = "ingglish-format-diff";
|
|
12
|
+
var NOT_FOUND_CLASS = "ingglish-not-found";
|
|
13
|
+
|
|
14
|
+
// src/traversal/browser.ts
|
|
15
|
+
function isBrowser() {
|
|
16
|
+
return typeof document !== "undefined" && globalThis.window !== void 0;
|
|
17
|
+
}
|
|
18
|
+
function requireBrowser() {
|
|
19
|
+
if (!isBrowser()) {
|
|
20
|
+
throw new Error("DOM translation requires a browser environment");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/traversal/extract.ts
|
|
25
|
+
import { normalizeApostrophes, WORD_SPLIT_REGEX, WORD_TEST_REGEX } from "@ingglish/normalize";
|
|
26
|
+
function extractWordsFromNodes(textNodes) {
|
|
27
|
+
const uniqueWords = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const node of textNodes) {
|
|
29
|
+
const text = node.textContent ?? "";
|
|
30
|
+
const normalized = normalizeApostrophes(text);
|
|
31
|
+
const tokens = normalized.split(WORD_SPLIT_REGEX);
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
if (token !== "" && WORD_TEST_REGEX.test(token)) {
|
|
34
|
+
uniqueWords.add(token.toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...uniqueWords];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/traversal/skip-rules.ts
|
|
42
|
+
var DEFAULT_SKIP_TAGS = [
|
|
43
|
+
"SCRIPT",
|
|
44
|
+
"STYLE",
|
|
45
|
+
"CODE",
|
|
46
|
+
"PRE",
|
|
47
|
+
"KBD",
|
|
48
|
+
"SAMP",
|
|
49
|
+
"VAR",
|
|
50
|
+
"NOSCRIPT",
|
|
51
|
+
"TEXTAREA",
|
|
52
|
+
"INPUT",
|
|
53
|
+
"SVG",
|
|
54
|
+
"MATH",
|
|
55
|
+
"CANVAS"
|
|
56
|
+
];
|
|
57
|
+
var DEFAULT_SKIP_TAGS_SET = new Set(DEFAULT_SKIP_TAGS);
|
|
58
|
+
var DEFAULT_SKIP_CLASSES = ["no-translate", "notranslate"];
|
|
59
|
+
var DEFAULT_SKIP_CLASSES_SET = new Set(DEFAULT_SKIP_CLASSES);
|
|
60
|
+
var TRANSLATABLE_ATTRIBUTES = [
|
|
61
|
+
"title",
|
|
62
|
+
"alt",
|
|
63
|
+
"placeholder",
|
|
64
|
+
"aria-label",
|
|
65
|
+
"aria-description"
|
|
66
|
+
];
|
|
67
|
+
function shouldSkipElement(element, skipTags, skipClasses) {
|
|
68
|
+
const tagsSet = skipTags === DEFAULT_SKIP_TAGS ? DEFAULT_SKIP_TAGS_SET : new Set(skipTags);
|
|
69
|
+
const classesSet = skipClasses === DEFAULT_SKIP_CLASSES ? DEFAULT_SKIP_CLASSES_SET : new Set(skipClasses);
|
|
70
|
+
return checkElementSkip(element, tagsSet, classesSet);
|
|
71
|
+
}
|
|
72
|
+
function checkElementSkip(element, skipTagsSet, skipClassesSet) {
|
|
73
|
+
if (skipTagsSet.has(element.tagName)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const classList = element.classList;
|
|
77
|
+
const classCount = classList.length;
|
|
78
|
+
for (let i = 0; i < classCount; i++) {
|
|
79
|
+
if (skipClassesSet.has(classList[i])) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (element.getAttribute("contenteditable") === "true") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (element.hasAttribute(ATTR_SKIP)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (element.hasAttribute(ATTR_ORIGINAL_WORD)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/traversal/text-nodes.ts
|
|
96
|
+
function collectTextNodes(root, skipTags = DEFAULT_SKIP_TAGS, skipClasses = DEFAULT_SKIP_CLASSES) {
|
|
97
|
+
requireBrowser();
|
|
98
|
+
const textNodes = [];
|
|
99
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
|
|
100
|
+
acceptNode(node) {
|
|
101
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
102
|
+
return shouldSkipElement(node, skipTags, skipClasses) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_SKIP;
|
|
103
|
+
}
|
|
104
|
+
const text = node.textContent?.trim() ?? "";
|
|
105
|
+
return text.length > 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
while (walker.nextNode()) {
|
|
109
|
+
if (walker.currentNode.nodeType === Node.TEXT_NODE) {
|
|
110
|
+
textNodes.push(walker.currentNode);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return textNodes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/traversal/tooltip.ts
|
|
117
|
+
var TOOLTIP_BEHAVIOR_ID = "ingglish-tooltip-behavior";
|
|
118
|
+
var TOOLTIP_STYLES = `
|
|
119
|
+
.${WORD_SPAN_CLASS} {
|
|
120
|
+
cursor: help;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ingglish-tooltip {
|
|
124
|
+
position: fixed !important;
|
|
125
|
+
background: #333 !important;
|
|
126
|
+
color: #fff !important;
|
|
127
|
+
padding: 4px 8px !important;
|
|
128
|
+
border-radius: 4px !important;
|
|
129
|
+
font-size: 12px !important;
|
|
130
|
+
font-family: system-ui, -apple-system, sans-serif !important;
|
|
131
|
+
line-height: 1.4 !important;
|
|
132
|
+
white-space: nowrap !important;
|
|
133
|
+
z-index: 2147483647 !important;
|
|
134
|
+
pointer-events: none !important;
|
|
135
|
+
opacity: 0;
|
|
136
|
+
animation: ingglish-tooltip-fade-in 0.15s ease-out forwards;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ingglish-tooltip::after {
|
|
140
|
+
content: '' !important;
|
|
141
|
+
position: absolute !important;
|
|
142
|
+
left: var(--arrow-left, 50%) !important;
|
|
143
|
+
transform: translateX(-50%) !important;
|
|
144
|
+
border: 5px solid transparent !important;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ingglish-tooltip--above::after {
|
|
148
|
+
top: 100% !important;
|
|
149
|
+
border-top-color: #333 !important;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.ingglish-tooltip--below::after {
|
|
153
|
+
bottom: 100% !important;
|
|
154
|
+
border-bottom-color: #333 !important;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@keyframes ingglish-tooltip-fade-in {
|
|
158
|
+
to { opacity: 1; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.${FORMAT_DIFF_CLASS} {
|
|
162
|
+
border-bottom: 1.5px dotted currentColor;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.${NOT_FOUND_CLASS} {
|
|
166
|
+
opacity: 0.55;
|
|
167
|
+
text-decoration: underline dotted currentColor;
|
|
168
|
+
text-decoration-thickness: 1.5px;
|
|
169
|
+
text-underline-offset: 0.2em;
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
function injectTooltipBehavior(targetDoc = document) {
|
|
173
|
+
if (targetDoc.documentElement.hasAttribute(`data-${TOOLTIP_BEHAVIOR_ID}`)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
targetDoc.documentElement.setAttribute(`data-${TOOLTIP_BEHAVIOR_ID}`, "true");
|
|
177
|
+
let activeTooltip = null;
|
|
178
|
+
let activeWord = null;
|
|
179
|
+
function removeTooltip() {
|
|
180
|
+
if (activeTooltip) {
|
|
181
|
+
activeTooltip.remove();
|
|
182
|
+
activeTooltip = null;
|
|
183
|
+
activeWord = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
targetDoc.addEventListener(
|
|
187
|
+
"mouseover",
|
|
188
|
+
(e) => {
|
|
189
|
+
const word = e.target.closest?.(`.${WORD_SPAN_CLASS}`);
|
|
190
|
+
if (word === activeWord) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
removeTooltip();
|
|
194
|
+
if (!word) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const orig = word.getAttribute(ATTR_ORIGINAL_WORD);
|
|
198
|
+
if (orig === null || orig === "") {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const tooltip = targetDoc.createElement("div");
|
|
202
|
+
tooltip.className = "ingglish-tooltip";
|
|
203
|
+
tooltip.textContent = orig;
|
|
204
|
+
targetDoc.body.append(tooltip);
|
|
205
|
+
activeTooltip = tooltip;
|
|
206
|
+
activeWord = word;
|
|
207
|
+
const wordRect = word.getBoundingClientRect();
|
|
208
|
+
const tipRect = tooltip.getBoundingClientRect();
|
|
209
|
+
const viewportWidth = targetDoc.defaultView?.innerWidth ?? 0;
|
|
210
|
+
const showAbove = wordRect.top > tipRect.height + 10;
|
|
211
|
+
tooltip.classList.add(showAbove ? "ingglish-tooltip--above" : "ingglish-tooltip--below");
|
|
212
|
+
const top = showAbove ? wordRect.top - tipRect.height - 5 : wordRect.bottom + 5;
|
|
213
|
+
tooltip.style.top = `${top}px`;
|
|
214
|
+
let left = wordRect.left + wordRect.width / 2 - tipRect.width / 2;
|
|
215
|
+
left = Math.max(4, Math.min(left, viewportWidth - tipRect.width - 4));
|
|
216
|
+
tooltip.style.left = `${left}px`;
|
|
217
|
+
const arrowLeft = wordRect.left + wordRect.width / 2 - left;
|
|
218
|
+
tooltip.style.setProperty("--arrow-left", `${arrowLeft}px`);
|
|
219
|
+
},
|
|
220
|
+
true
|
|
221
|
+
);
|
|
222
|
+
targetDoc.addEventListener("scroll", removeTooltip, true);
|
|
223
|
+
}
|
|
224
|
+
function injectTooltipStyles(targetDoc = document) {
|
|
225
|
+
if (targetDoc.querySelector(`#${TOOLTIP_STYLES_ID}`)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const style = targetDoc.createElement("style");
|
|
229
|
+
style.id = TOOLTIP_STYLES_ID;
|
|
230
|
+
style.textContent = TOOLTIP_STYLES;
|
|
231
|
+
targetDoc.head?.append(style);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/translate/chunked.ts
|
|
235
|
+
function processChunked(items, processFn, chunkSize, onProgress, syncThreshold = 0) {
|
|
236
|
+
const total = items.length;
|
|
237
|
+
if (total === 0) {
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
if (total <= syncThreshold) {
|
|
241
|
+
for (let i = 0; i < total; i++) {
|
|
242
|
+
processFn(items[i]);
|
|
243
|
+
if (onProgress) {
|
|
244
|
+
onProgress(i + 1, total);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return Promise.resolve();
|
|
248
|
+
}
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
let index = 0;
|
|
251
|
+
function processChunk() {
|
|
252
|
+
const endIndex = Math.min(index + chunkSize, total);
|
|
253
|
+
while (index < endIndex) {
|
|
254
|
+
processFn(items[index]);
|
|
255
|
+
index++;
|
|
256
|
+
if (onProgress) {
|
|
257
|
+
onProgress(index, total);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (index < total) {
|
|
261
|
+
requestAnimationFrame(processChunk);
|
|
262
|
+
} else {
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
requestAnimationFrame(processChunk);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/translate/tooltip-fragment.ts
|
|
271
|
+
import { translateSyncWithMapping } from "ingglish";
|
|
272
|
+
import {
|
|
273
|
+
applyCasePattern,
|
|
274
|
+
detectCasePattern,
|
|
275
|
+
normalizeApostrophes as normalizeApostrophes2,
|
|
276
|
+
WORD_SPLIT_REGEX as WORD_SPLIT_REGEX2,
|
|
277
|
+
WORD_TEST_REGEX as WORD_TEST_REGEX2
|
|
278
|
+
} from "@ingglish/normalize";
|
|
279
|
+
var templateSpan = null;
|
|
280
|
+
function createTooltipFragment(text, format = "ingglish") {
|
|
281
|
+
const tokens = translateSyncWithMapping(text, { format });
|
|
282
|
+
const stdTokens = format === "ingglish" ? null : translateSyncWithMapping(text, { format: "ingglish" });
|
|
283
|
+
return buildTooltipFragment(
|
|
284
|
+
tokens.map((token, i) => {
|
|
285
|
+
if (token.isWord && token.original !== token.translated) {
|
|
286
|
+
const stdSpelling = stdTokens?.[i]?.translated;
|
|
287
|
+
const isDiff = stdSpelling !== void 0 && stdSpelling.toLowerCase() !== token.translated.toLowerCase();
|
|
288
|
+
const notFound = !token.matched;
|
|
289
|
+
const tooltip = isDiff ? `${token.original} (Ingglish: ${stdSpelling})` : token.original;
|
|
290
|
+
return { isDiff, notFound, text: token.translated, tooltip };
|
|
291
|
+
}
|
|
292
|
+
return { text: token.translated };
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function createTooltipFragmentFromMap(text, translations) {
|
|
297
|
+
const normalized = normalizeApostrophes2(text);
|
|
298
|
+
const tokens = normalized.split(WORD_SPLIT_REGEX2);
|
|
299
|
+
return buildTooltipFragment(
|
|
300
|
+
tokens.filter(Boolean).map((token) => {
|
|
301
|
+
if (WORD_TEST_REGEX2.test(token)) {
|
|
302
|
+
const lowerToken = token.toLowerCase();
|
|
303
|
+
const translated = translations[lowerToken];
|
|
304
|
+
if (translated !== void 0 && translated !== lowerToken) {
|
|
305
|
+
const pattern = detectCasePattern(token);
|
|
306
|
+
return { text: applyCasePattern(translated, pattern, token), tooltip: token };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { text: token };
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
function createTooltipFragmentFromTokens(tokens) {
|
|
314
|
+
return buildTooltipFragment(
|
|
315
|
+
tokens.map((token) => {
|
|
316
|
+
if (token.isWord && token.original !== token.translated) {
|
|
317
|
+
return { notFound: !token.matched, text: token.translated, tooltip: token.original };
|
|
318
|
+
}
|
|
319
|
+
if (token.isWord && !token.matched) {
|
|
320
|
+
return { notFound: true, text: token.translated, tooltip: token.original };
|
|
321
|
+
}
|
|
322
|
+
return { text: token.translated };
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
function buildTooltipFragment(items) {
|
|
327
|
+
const fragment = document.createDocumentFragment();
|
|
328
|
+
let pendingText = "";
|
|
329
|
+
for (const item of items) {
|
|
330
|
+
if (item.tooltip === void 0) {
|
|
331
|
+
pendingText += item.text;
|
|
332
|
+
} else {
|
|
333
|
+
if (pendingText) {
|
|
334
|
+
fragment.append(document.createTextNode(pendingText));
|
|
335
|
+
pendingText = "";
|
|
336
|
+
}
|
|
337
|
+
const span = getTemplateSpan().cloneNode(false);
|
|
338
|
+
span.setAttribute(ATTR_ORIGINAL_WORD, item.tooltip);
|
|
339
|
+
if (item.isDiff === true) {
|
|
340
|
+
span.classList.add(FORMAT_DIFF_CLASS);
|
|
341
|
+
}
|
|
342
|
+
if (item.notFound === true) {
|
|
343
|
+
span.classList.add(NOT_FOUND_CLASS);
|
|
344
|
+
}
|
|
345
|
+
span.textContent = item.text;
|
|
346
|
+
fragment.append(span);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (pendingText) {
|
|
350
|
+
fragment.append(document.createTextNode(pendingText));
|
|
351
|
+
}
|
|
352
|
+
return fragment;
|
|
353
|
+
}
|
|
354
|
+
function getTemplateSpan() {
|
|
355
|
+
if (templateSpan === null) {
|
|
356
|
+
templateSpan = document.createElement("span");
|
|
357
|
+
templateSpan.className = WORD_SPAN_CLASS;
|
|
358
|
+
}
|
|
359
|
+
return templateSpan;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/translate/apply-map.ts
|
|
363
|
+
var DEFAULT_CHUNK_SIZE = 100;
|
|
364
|
+
var SYNC_THRESHOLD = 500;
|
|
365
|
+
var WORD_REGEX = /(?<!\d)[a-zA-Z\u00C0-\u024F']+(?!\d)/g;
|
|
366
|
+
function applyTranslationsMap(root, translations, options = {}) {
|
|
367
|
+
requireBrowser();
|
|
368
|
+
const {
|
|
369
|
+
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
370
|
+
onProgress,
|
|
371
|
+
showTooltips = false,
|
|
372
|
+
textNodes: preCollectedNodes
|
|
373
|
+
} = options;
|
|
374
|
+
const targetDoc = root instanceof Document ? root : root.ownerDocument;
|
|
375
|
+
if (showTooltips && targetDoc !== null) {
|
|
376
|
+
injectTooltipStyles(targetDoc);
|
|
377
|
+
injectTooltipBehavior(targetDoc);
|
|
378
|
+
}
|
|
379
|
+
const textNodes = preCollectedNodes ?? collectTextNodes(root);
|
|
380
|
+
return processChunked(
|
|
381
|
+
textNodes,
|
|
382
|
+
(node) => {
|
|
383
|
+
processTextNode(node, translations, showTooltips);
|
|
384
|
+
},
|
|
385
|
+
chunkSize,
|
|
386
|
+
onProgress,
|
|
387
|
+
SYNC_THRESHOLD
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
function processTextNode(textNode, translations, showTooltips) {
|
|
391
|
+
const parent = textNode.parentElement;
|
|
392
|
+
if (parent && !parent.hasAttribute(ATTR_ORIGINAL_CONTENT)) {
|
|
393
|
+
parent.setAttribute(ATTR_ORIGINAL_CONTENT, textNode.textContent ?? "");
|
|
394
|
+
}
|
|
395
|
+
if (showTooltips) {
|
|
396
|
+
const fragment = createTooltipFragmentFromMap(textNode.textContent ?? "", translations);
|
|
397
|
+
textNode.replaceWith(fragment);
|
|
398
|
+
} else {
|
|
399
|
+
const text = textNode.textContent ?? "";
|
|
400
|
+
const normalized = normalizeApostrophes3(text);
|
|
401
|
+
let result = "";
|
|
402
|
+
let lastIndex = 0;
|
|
403
|
+
let match;
|
|
404
|
+
WORD_REGEX.lastIndex = 0;
|
|
405
|
+
while ((match = WORD_REGEX.exec(normalized)) !== null) {
|
|
406
|
+
result += normalized.slice(lastIndex, match.index);
|
|
407
|
+
lastIndex = match.index + match[0].length;
|
|
408
|
+
const word = match[0];
|
|
409
|
+
const translated = translations[word.toLowerCase()];
|
|
410
|
+
if (translated === void 0) {
|
|
411
|
+
result += word;
|
|
412
|
+
} else {
|
|
413
|
+
const pattern = detectCasePattern2(word);
|
|
414
|
+
result += applyCasePattern2(translated, pattern, word);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
result += normalized.slice(lastIndex);
|
|
418
|
+
textNode.textContent = result;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/translate/restore.ts
|
|
423
|
+
function restoreDOM(root) {
|
|
424
|
+
requireBrowser();
|
|
425
|
+
const wordSpans = Array.from(
|
|
426
|
+
root.querySelectorAll(`.${WORD_SPAN_CLASS}[${ATTR_ORIGINAL_WORD}]`)
|
|
427
|
+
);
|
|
428
|
+
for (const span of wordSpans) {
|
|
429
|
+
const originalWord = span.getAttribute(ATTR_ORIGINAL_WORD);
|
|
430
|
+
if (originalWord !== null) {
|
|
431
|
+
const textNode = document.createTextNode(originalWord);
|
|
432
|
+
span.replaceWith(textNode);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const elementsWithOriginal = Array.from(
|
|
436
|
+
root.querySelectorAll(`[${ATTR_ORIGINAL_CONTENT}]`)
|
|
437
|
+
);
|
|
438
|
+
for (const element of elementsWithOriginal) {
|
|
439
|
+
element.removeAttribute(ATTR_ORIGINAL_CONTENT);
|
|
440
|
+
}
|
|
441
|
+
const attrSelector = TRANSLATABLE_ATTRIBUTES.map(
|
|
442
|
+
(attr) => `[${ATTR_ORIGINAL_PREFIX}${attr}]`
|
|
443
|
+
).join(",");
|
|
444
|
+
const elementsWithTranslatedAttrs = Array.from(root.querySelectorAll(attrSelector));
|
|
445
|
+
for (const element of elementsWithTranslatedAttrs) {
|
|
446
|
+
for (const attrName of TRANSLATABLE_ATTRIBUTES) {
|
|
447
|
+
const originalAttrName = `${ATTR_ORIGINAL_PREFIX}${attrName}`;
|
|
448
|
+
const originalValue = element.getAttribute(originalAttrName);
|
|
449
|
+
if (originalValue !== null) {
|
|
450
|
+
element.setAttribute(attrName, originalValue);
|
|
451
|
+
element.removeAttribute(originalAttrName);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/translate/translator.ts
|
|
458
|
+
import { translate, translateSync } from "ingglish";
|
|
459
|
+
var DEFAULT_CHUNK_SIZE2 = 100;
|
|
460
|
+
async function translateDOM(root, options = {}) {
|
|
461
|
+
if (!options.translateWithMappingFn) {
|
|
462
|
+
await translate("");
|
|
463
|
+
}
|
|
464
|
+
const result = translateDOMSync(root, options);
|
|
465
|
+
if (result instanceof Promise) {
|
|
466
|
+
await result;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function translateDOMSync(root, options = {}) {
|
|
470
|
+
requireBrowser();
|
|
471
|
+
const {
|
|
472
|
+
chunked = false,
|
|
473
|
+
chunkSize = DEFAULT_CHUNK_SIZE2,
|
|
474
|
+
onProgress,
|
|
475
|
+
outputFormat = "ingglish",
|
|
476
|
+
showTooltips = false,
|
|
477
|
+
skipClasses = DEFAULT_SKIP_CLASSES,
|
|
478
|
+
skipTags = DEFAULT_SKIP_TAGS,
|
|
479
|
+
translateAttributes = true,
|
|
480
|
+
translateWithMappingFn
|
|
481
|
+
} = options;
|
|
482
|
+
const targetDoc = root instanceof Document ? root : root.ownerDocument;
|
|
483
|
+
if (showTooltips && targetDoc !== null) {
|
|
484
|
+
injectTooltipStyles(targetDoc);
|
|
485
|
+
injectTooltipBehavior(targetDoc);
|
|
486
|
+
}
|
|
487
|
+
const textNodes = collectTextNodes(root, skipTags, skipClasses);
|
|
488
|
+
const totalNodes = textNodes.length;
|
|
489
|
+
const customTranslateFn = translateWithMappingFn ? (text, format) => translateWithMappingFn(text, format).map((t) => t.translated).join("") : void 0;
|
|
490
|
+
if (translateAttributes) {
|
|
491
|
+
translateElementAttributes(root, skipTags, skipClasses, outputFormat, customTranslateFn);
|
|
492
|
+
}
|
|
493
|
+
if (chunked) {
|
|
494
|
+
return processChunked(
|
|
495
|
+
textNodes,
|
|
496
|
+
(node) => {
|
|
497
|
+
translateTextNode(
|
|
498
|
+
node,
|
|
499
|
+
showTooltips,
|
|
500
|
+
outputFormat,
|
|
501
|
+
customTranslateFn,
|
|
502
|
+
translateWithMappingFn
|
|
503
|
+
);
|
|
504
|
+
},
|
|
505
|
+
chunkSize,
|
|
506
|
+
onProgress
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
for (let i = 0; i < totalNodes; i++) {
|
|
510
|
+
translateTextNode(
|
|
511
|
+
textNodes[i],
|
|
512
|
+
showTooltips,
|
|
513
|
+
outputFormat,
|
|
514
|
+
customTranslateFn,
|
|
515
|
+
translateWithMappingFn
|
|
516
|
+
);
|
|
517
|
+
if (onProgress) {
|
|
518
|
+
onProgress(i + 1, totalNodes);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function translateElementAttributes(root, skipTags, skipClasses, format = "ingglish", customTranslateFn) {
|
|
523
|
+
const doTranslate = customTranslateFn ?? ((text, fmt) => translateSync(text, { format: fmt }));
|
|
524
|
+
const attrSelector = TRANSLATABLE_ATTRIBUTES.map((attr) => `[${attr}]`).join(",");
|
|
525
|
+
const elements = Array.from(root.querySelectorAll(attrSelector));
|
|
526
|
+
for (const element of elements) {
|
|
527
|
+
if (shouldSkipElement(element, skipTags, skipClasses)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
for (const attrName of TRANSLATABLE_ATTRIBUTES) {
|
|
531
|
+
const attrValue = element.getAttribute(attrName);
|
|
532
|
+
if (attrValue !== null && attrValue.length > 0) {
|
|
533
|
+
const originalAttrName = `${ATTR_ORIGINAL_PREFIX}${attrName}`;
|
|
534
|
+
if (!element.hasAttribute(originalAttrName)) {
|
|
535
|
+
element.setAttribute(originalAttrName, attrValue);
|
|
536
|
+
}
|
|
537
|
+
element.setAttribute(attrName, doTranslate(attrValue, format));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function translateTextNode(textNode, showTooltips, outputFormat, customTranslateFn, customMappingFn) {
|
|
543
|
+
const originalText = textNode.textContent;
|
|
544
|
+
if (!originalText) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const parent = textNode.parentElement;
|
|
548
|
+
if (showTooltips) {
|
|
549
|
+
const fragment = customMappingFn ? createTooltipFragmentFromTokens(customMappingFn(originalText, outputFormat)) : createTooltipFragment(originalText, outputFormat);
|
|
550
|
+
if (parent && !parent.hasAttribute(ATTR_ORIGINAL_CONTENT)) {
|
|
551
|
+
parent.setAttribute(ATTR_ORIGINAL_CONTENT, originalText);
|
|
552
|
+
}
|
|
553
|
+
textNode.replaceWith(fragment);
|
|
554
|
+
} else {
|
|
555
|
+
if (parent && !parent.hasAttribute(ATTR_ORIGINAL_CONTENT)) {
|
|
556
|
+
parent.setAttribute(ATTR_ORIGINAL_CONTENT, originalText);
|
|
557
|
+
}
|
|
558
|
+
const doTranslate = customTranslateFn ?? ((text, fmt) => translateSync(text, { format: fmt }));
|
|
559
|
+
textNode.textContent = doTranslate(originalText, outputFormat);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
export {
|
|
563
|
+
DEFAULT_SKIP_CLASSES,
|
|
564
|
+
DEFAULT_SKIP_TAGS,
|
|
565
|
+
applyTranslationsMap,
|
|
566
|
+
collectTextNodes,
|
|
567
|
+
extractWordsFromNodes,
|
|
568
|
+
injectTooltipBehavior,
|
|
569
|
+
injectTooltipStyles,
|
|
570
|
+
restoreDOM,
|
|
571
|
+
translateDOM
|
|
572
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingglish/dom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DOM translation utilities for Ingglish",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"build:fast": "tsup src/index.ts --format esm",
|
|
31
|
+
"lint": "eslint --cache src",
|
|
32
|
+
"test": "vitest run --no-color",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@ingglish/ipa": "^0.1.0",
|
|
38
|
+
"@ingglish/normalize": "^0.1.0",
|
|
39
|
+
"@ingglish/phonemes": "^0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"ingglish": "^0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.3.0",
|
|
46
|
+
"jsdom": "^28.1.0",
|
|
47
|
+
"vite": "^7.3.1"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"dom",
|
|
51
|
+
"translation",
|
|
52
|
+
"phonetic",
|
|
53
|
+
"english",
|
|
54
|
+
"browser"
|
|
55
|
+
],
|
|
56
|
+
"author": "Paul Tarjan",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/ptarjan/ingglish.git",
|
|
61
|
+
"directory": "packages/dom"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/ptarjan/ingglish#readme",
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/ptarjan/ingglish/issues"
|
|
66
|
+
}
|
|
67
|
+
}
|