@worthy-ventures/metaglotta-observer 1.0.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.ts +4 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/marks.d.ts +50 -0
- package/dist/marks.js +122 -0
- package/dist/marks.js.map +1 -0
- package/dist/observer.d.ts +77 -0
- package/dist/observer.js +408 -0
- package/dist/observer.js.map +1 -0
- package/dist-esm/index.js +3 -0
- package/dist-esm/index.js.map +1 -0
- package/dist-esm/marks.js +115 -0
- package/dist-esm/marks.js.map +1 -0
- package/dist-esm/observer.js +405 -0
- package/dist-esm/observer.js.map +1 -0
- package/dist-esm/package.json +1 -0
- package/package.json +35 -0
- package/src/index.ts +4 -0
- package/src/marks.test.ts +125 -0
- package/src/marks.ts +128 -0
- package/src/observer.test.ts +522 -0
- package/src/observer.ts +471 -0
package/dist/observer.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createObserver = createObserver;
|
|
4
|
+
const marks_js_1 = require("./marks.js");
|
|
5
|
+
const MODIFIER_FLAG = {
|
|
6
|
+
Alt: 'altKey',
|
|
7
|
+
Control: 'ctrlKey',
|
|
8
|
+
Shift: 'shiftKey',
|
|
9
|
+
Meta: 'metaKey',
|
|
10
|
+
};
|
|
11
|
+
/** Where a translation plausibly lands other than in text. */
|
|
12
|
+
const DEFAULT_ATTRIBUTES = ['title', 'placeholder', 'aria-label', 'alt', 'value', 'label'];
|
|
13
|
+
/** Never worth walking into: their text is not for reading. */
|
|
14
|
+
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'TEXTAREA']);
|
|
15
|
+
const HIGHLIGHT_CLASS = 'mg-observer-highlight';
|
|
16
|
+
/**
|
|
17
|
+
* How many observers are watching, so a second one can say so.
|
|
18
|
+
*
|
|
19
|
+
* Two on the same document silently break each other: marks are taken OUT of a node as it is
|
|
20
|
+
* scanned, so whichever observer's mutation callback runs first absorbs them and the other
|
|
21
|
+
* registers nothing at all. ALT+click then does nothing, with no error to explain it.
|
|
22
|
+
*
|
|
23
|
+
* One page only ever has one. Where this bites is a hot reload, or a test that forgot to stop
|
|
24
|
+
* the previous instance - both of which look like the feature is broken rather than
|
|
25
|
+
* double-installed.
|
|
26
|
+
*/
|
|
27
|
+
let watching = 0;
|
|
28
|
+
function createObserver(options) {
|
|
29
|
+
const armWith = options.keys ?? ['Alt'];
|
|
30
|
+
const attributes = options.attributes ?? DEFAULT_ATTRIBUTES;
|
|
31
|
+
const root = options.root ?? document;
|
|
32
|
+
/**
|
|
33
|
+
* Tech Blue, the brand accent.
|
|
34
|
+
*
|
|
35
|
+
* An outline is one of the few places the light blue is right: it sits on top of the page
|
|
36
|
+
* rather than under text, so its 2.14:1 against white does not matter - what matters is
|
|
37
|
+
* that it reads as ours. Pass highlightColor to override it on a page it disappears into.
|
|
38
|
+
*/
|
|
39
|
+
const highlightColor = options.highlightColor ?? '#38bdf8';
|
|
40
|
+
const highlightWidth = options.highlightWidth ?? 2;
|
|
41
|
+
const registry = new Map();
|
|
42
|
+
const held = new Set();
|
|
43
|
+
let mutations;
|
|
44
|
+
let listeners = [];
|
|
45
|
+
let cursor;
|
|
46
|
+
let outlined;
|
|
47
|
+
const ignored = (element) => Boolean(options.ignore?.(element)) || SKIP_TAGS.has(element.tagName);
|
|
48
|
+
const withinIgnored = (node) => {
|
|
49
|
+
let at = node;
|
|
50
|
+
while (at) {
|
|
51
|
+
if (at.nodeType === Node.ELEMENT_NODE && ignored(at))
|
|
52
|
+
return true;
|
|
53
|
+
at = at.parentNode;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
// ---- 2. scan ----------------------------------------------------------------------
|
|
58
|
+
function register(element, node, keys) {
|
|
59
|
+
let entry = registry.get(element);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
entry = { element, nodes: new Map() };
|
|
62
|
+
registry.set(element, entry);
|
|
63
|
+
}
|
|
64
|
+
entry.nodes.set(node, keys);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reads the keys out and puts the visible text back.
|
|
68
|
+
*
|
|
69
|
+
* Stripping is not tidiness. Marks left in the DOM are copied by Ctrl+C, submitted in
|
|
70
|
+
* form values, and compared against by application code that has no idea they are there -
|
|
71
|
+
* and because they are invisible, the resulting bug looks like magic. Writing the node
|
|
72
|
+
* back triggers another mutation, which is harmless: the text is no longer marked, so the
|
|
73
|
+
* next pass skips it.
|
|
74
|
+
*/
|
|
75
|
+
function absorb(node) {
|
|
76
|
+
const raw = node.nodeValue;
|
|
77
|
+
if (!raw || !(0, marks_js_1.isMarked)(raw))
|
|
78
|
+
return;
|
|
79
|
+
const owner = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentElement;
|
|
80
|
+
if (!owner || withinIgnored(owner))
|
|
81
|
+
return;
|
|
82
|
+
const { text, keys } = (0, marks_js_1.readMarks)(raw);
|
|
83
|
+
node.nodeValue = text;
|
|
84
|
+
if (keys.length)
|
|
85
|
+
register(owner, node, keys);
|
|
86
|
+
}
|
|
87
|
+
function scan(node) {
|
|
88
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
89
|
+
absorb(node);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_NODE)
|
|
93
|
+
return;
|
|
94
|
+
if (node.nodeType === Node.ELEMENT_NODE && ignored(node))
|
|
95
|
+
return;
|
|
96
|
+
const elements = [node, ...Array.from(node.querySelectorAll?.('*') ?? [])];
|
|
97
|
+
for (const element of elements) {
|
|
98
|
+
if (element.nodeType !== Node.ELEMENT_NODE || ignored(element))
|
|
99
|
+
continue;
|
|
100
|
+
for (const name of attributes) {
|
|
101
|
+
const attribute = element.getAttributeNode?.(name);
|
|
102
|
+
if (attribute)
|
|
103
|
+
absorb(attribute);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
|
|
107
|
+
acceptNode: text => (0, marks_js_1.isMarked)(text.nodeValue ?? '') && !SKIP_TAGS.has(text.parentElement?.tagName ?? '')
|
|
108
|
+
? NodeFilter.FILTER_ACCEPT
|
|
109
|
+
: NodeFilter.FILTER_REJECT,
|
|
110
|
+
});
|
|
111
|
+
const found = [];
|
|
112
|
+
while (walker.nextNode())
|
|
113
|
+
found.push(walker.currentNode);
|
|
114
|
+
for (const text of found)
|
|
115
|
+
absorb(text);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Forgets what has left the page.
|
|
119
|
+
*
|
|
120
|
+
* Without this the registry holds every element the page has ever rendered - a leak that
|
|
121
|
+
* also makes findPositions() report boxes for text nobody can see.
|
|
122
|
+
*/
|
|
123
|
+
function forget() {
|
|
124
|
+
for (const [element, entry] of registry) {
|
|
125
|
+
if (!element.isConnected) {
|
|
126
|
+
unoutline(element);
|
|
127
|
+
registry.delete(element);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const node of entry.nodes.keys()) {
|
|
131
|
+
const stillThere = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement?.isConnected : node.isConnected;
|
|
132
|
+
if (!stillThere)
|
|
133
|
+
entry.nodes.delete(node);
|
|
134
|
+
}
|
|
135
|
+
if (entry.nodes.size === 0) {
|
|
136
|
+
unoutline(element);
|
|
137
|
+
registry.delete(element);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---- 3. point ---------------------------------------------------------------------
|
|
142
|
+
function armed() {
|
|
143
|
+
return armWith.every(key => held.has(key));
|
|
144
|
+
}
|
|
145
|
+
function trackModifiers(event) {
|
|
146
|
+
for (const name of Object.keys(MODIFIER_FLAG)) {
|
|
147
|
+
if (event[MODIFIER_FLAG[name]])
|
|
148
|
+
held.add(name);
|
|
149
|
+
else
|
|
150
|
+
held.delete(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function registeredAt(x, y) {
|
|
154
|
+
for (const candidate of document.elementsFromPoint(x, y)) {
|
|
155
|
+
let at = candidate;
|
|
156
|
+
while (at) {
|
|
157
|
+
if (ignored(at))
|
|
158
|
+
return undefined;
|
|
159
|
+
if (registry.has(at))
|
|
160
|
+
return at;
|
|
161
|
+
at = at.parentElement;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
function outline(element) {
|
|
167
|
+
if (outlined === element) {
|
|
168
|
+
if (element)
|
|
169
|
+
position(element);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (outlined)
|
|
173
|
+
unoutline(outlined);
|
|
174
|
+
outlined = element;
|
|
175
|
+
if (element)
|
|
176
|
+
position(element);
|
|
177
|
+
}
|
|
178
|
+
function position(element) {
|
|
179
|
+
const entry = registry.get(element);
|
|
180
|
+
if (!entry || !element.isConnected)
|
|
181
|
+
return;
|
|
182
|
+
if (!entry.outline) {
|
|
183
|
+
const box = document.createElement('div');
|
|
184
|
+
box.className = HIGHLIGHT_CLASS;
|
|
185
|
+
Object.assign(box.style, {
|
|
186
|
+
position: 'fixed',
|
|
187
|
+
boxSizing: 'content-box',
|
|
188
|
+
pointerEvents: 'none',
|
|
189
|
+
zIndex: String(Number.MAX_SAFE_INTEGER),
|
|
190
|
+
borderStyle: 'solid',
|
|
191
|
+
borderRadius: '4px',
|
|
192
|
+
borderColor: highlightColor,
|
|
193
|
+
borderWidth: `${highlightWidth}px`,
|
|
194
|
+
contain: 'layout',
|
|
195
|
+
});
|
|
196
|
+
document.body.appendChild(box);
|
|
197
|
+
entry.outline = box;
|
|
198
|
+
}
|
|
199
|
+
const shape = element.getBoundingClientRect();
|
|
200
|
+
Object.assign(entry.outline.style, {
|
|
201
|
+
top: `${shape.top - highlightWidth}px`,
|
|
202
|
+
left: `${shape.left - highlightWidth}px`,
|
|
203
|
+
width: `${shape.width}px`,
|
|
204
|
+
height: `${shape.height}px`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function unoutline(element) {
|
|
208
|
+
const entry = registry.get(element);
|
|
209
|
+
entry?.outline?.remove();
|
|
210
|
+
if (entry)
|
|
211
|
+
entry.outline = undefined;
|
|
212
|
+
if (outlined === element)
|
|
213
|
+
outlined = undefined;
|
|
214
|
+
}
|
|
215
|
+
function refresh() {
|
|
216
|
+
outline(armed() && cursor ? registeredAt(cursor.x, cursor.y) : undefined);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Swallows the page's own interaction while armed.
|
|
220
|
+
*
|
|
221
|
+
* ALT+clicking a "Delete patient" button has to open the dialog and nothing else, so the
|
|
222
|
+
* event is stopped in the capture phase - before the application's own listener - and on
|
|
223
|
+
* every mouse event, not only click: a hover handler that opens a menu would otherwise
|
|
224
|
+
* fire and cover the thing being clicked.
|
|
225
|
+
*/
|
|
226
|
+
function swallow(event) {
|
|
227
|
+
trackModifiers(event);
|
|
228
|
+
if (!armed())
|
|
229
|
+
return;
|
|
230
|
+
const target = event.target;
|
|
231
|
+
if (target && withinIgnored(target))
|
|
232
|
+
return;
|
|
233
|
+
event.stopPropagation();
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
}
|
|
236
|
+
function onClick(event) {
|
|
237
|
+
swallow(event);
|
|
238
|
+
cursor = { x: event.clientX, y: event.clientY };
|
|
239
|
+
if (!armed())
|
|
240
|
+
return;
|
|
241
|
+
const element = registeredAt(event.clientX, event.clientY);
|
|
242
|
+
if (!element)
|
|
243
|
+
return;
|
|
244
|
+
const keys = [...(registry.get(element)?.nodes.values() ?? [])].flat();
|
|
245
|
+
if (!keys.length)
|
|
246
|
+
return;
|
|
247
|
+
outline(undefined);
|
|
248
|
+
options.onClick(keys, element);
|
|
249
|
+
}
|
|
250
|
+
function listen() {
|
|
251
|
+
const on = (type, handler, passive = false) => {
|
|
252
|
+
const wrapped = handler;
|
|
253
|
+
document.addEventListener(type, wrapped, { capture: true, passive });
|
|
254
|
+
listeners.push(() => document.removeEventListener(type, wrapped, { capture: true }));
|
|
255
|
+
};
|
|
256
|
+
on('keydown', event => {
|
|
257
|
+
trackModifiers(event);
|
|
258
|
+
refresh();
|
|
259
|
+
});
|
|
260
|
+
on('keyup', event => {
|
|
261
|
+
trackModifiers(event);
|
|
262
|
+
refresh();
|
|
263
|
+
});
|
|
264
|
+
on('mousemove', event => {
|
|
265
|
+
trackModifiers(event);
|
|
266
|
+
cursor = { x: event.clientX, y: event.clientY };
|
|
267
|
+
refresh();
|
|
268
|
+
}, true);
|
|
269
|
+
on('scroll', () => refresh(), true);
|
|
270
|
+
on('click', onClick);
|
|
271
|
+
for (const type of ['mousedown', 'mouseup', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave']) {
|
|
272
|
+
on(type, swallow);
|
|
273
|
+
}
|
|
274
|
+
// A window that loses focus never sees the keyup, so the modifier would stay held.
|
|
275
|
+
const onBlur = () => {
|
|
276
|
+
held.clear();
|
|
277
|
+
refresh();
|
|
278
|
+
};
|
|
279
|
+
window.addEventListener('blur', onBlur);
|
|
280
|
+
listeners.push(() => window.removeEventListener('blur', onBlur));
|
|
281
|
+
}
|
|
282
|
+
// ---- the public surface -----------------------------------------------------------
|
|
283
|
+
return {
|
|
284
|
+
/**
|
|
285
|
+
* Pass as `decorate` to the runtime. Everything else follows from this.
|
|
286
|
+
*
|
|
287
|
+
* The namespace comes from the runtime, not from `props`: props hold what the caller
|
|
288
|
+
* asked with, which for most templates is nothing at all, while this is where the
|
|
289
|
+
* string was actually resolved from. Marking with the former is what made the dialog
|
|
290
|
+
* open on the default namespace with nothing in it.
|
|
291
|
+
*/
|
|
292
|
+
mark: (result, props, namespace) => (0, marks_js_1.mark)(result, { key: props.key, ns: namespace || undefined, defaultValue: props.defaultValue }),
|
|
293
|
+
run() {
|
|
294
|
+
if (mutations)
|
|
295
|
+
return;
|
|
296
|
+
watching += 1;
|
|
297
|
+
if (watching > 1) {
|
|
298
|
+
console.warn(`Metaglotta: ${watching} in-context observers are running at once. They will take each other's marks and ALT+click will stop finding keys - stop the previous one.`);
|
|
299
|
+
}
|
|
300
|
+
scan(root);
|
|
301
|
+
listen();
|
|
302
|
+
mutations = new MutationObserver(records => {
|
|
303
|
+
let anythingRemoved = false;
|
|
304
|
+
for (const record of records) {
|
|
305
|
+
if (record.type === 'characterData')
|
|
306
|
+
scan(record.target);
|
|
307
|
+
if (record.type === 'attributes')
|
|
308
|
+
scan(record.target);
|
|
309
|
+
if (record.type === 'childList') {
|
|
310
|
+
record.addedNodes.forEach(node => scan(node));
|
|
311
|
+
anythingRemoved || (anythingRemoved = record.removedNodes.length > 0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Swept by what is still connected rather than by what was reported: a node
|
|
315
|
+
// moved and a node deleted look the same in a mutation record, and only the
|
|
316
|
+
// DOM knows which happened.
|
|
317
|
+
if (anythingRemoved)
|
|
318
|
+
forget();
|
|
319
|
+
});
|
|
320
|
+
mutations.observe(root, {
|
|
321
|
+
childList: true,
|
|
322
|
+
subtree: true,
|
|
323
|
+
characterData: true,
|
|
324
|
+
attributes: true,
|
|
325
|
+
attributeFilter: attributes,
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
stop() {
|
|
329
|
+
if (mutations)
|
|
330
|
+
watching -= 1;
|
|
331
|
+
mutations?.disconnect();
|
|
332
|
+
mutations = undefined;
|
|
333
|
+
for (const off of listeners)
|
|
334
|
+
off();
|
|
335
|
+
listeners = [];
|
|
336
|
+
for (const element of [...registry.keys()])
|
|
337
|
+
unoutline(element);
|
|
338
|
+
registry.clear();
|
|
339
|
+
held.clear();
|
|
340
|
+
cursor = undefined;
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* Where a key is on screen, in viewport coordinates.
|
|
344
|
+
*
|
|
345
|
+
* This is what a screenshot's boxes are drawn from, so the order matters: sorted by
|
|
346
|
+
* position in the document, because the dialog labels them in the order it gets them
|
|
347
|
+
* and a reader expects the first box to be the first occurrence.
|
|
348
|
+
*/
|
|
349
|
+
findPositions(key, ns) {
|
|
350
|
+
const matching = [...registry.values()].filter(entry => [...entry.nodes.values()].flat().some(found => matches(found, key, ns)));
|
|
351
|
+
matching.sort((a, b) => a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
352
|
+
const positions = [];
|
|
353
|
+
for (const entry of matching) {
|
|
354
|
+
const shape = entry.element.getBoundingClientRect();
|
|
355
|
+
for (const found of [...entry.nodes.values()].flat()) {
|
|
356
|
+
if (!matches(found, key, ns))
|
|
357
|
+
continue;
|
|
358
|
+
positions.push({
|
|
359
|
+
key: found.key,
|
|
360
|
+
ns: found.ns ?? '',
|
|
361
|
+
position: { x: shape.x, y: shape.y, width: shape.width, height: shape.height },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return positions;
|
|
366
|
+
},
|
|
367
|
+
/** Outlines every place a key appears. Returns how to put it back. */
|
|
368
|
+
highlight(key, ns) {
|
|
369
|
+
const elements = [...registry.values()]
|
|
370
|
+
.filter(entry => [...entry.nodes.values()].flat().some(found => matches(found, key, ns)))
|
|
371
|
+
.map(entry => entry.element);
|
|
372
|
+
for (const element of elements)
|
|
373
|
+
position(element);
|
|
374
|
+
return {
|
|
375
|
+
unhighlight: () => {
|
|
376
|
+
for (const element of elements)
|
|
377
|
+
unoutline(element);
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
/**
|
|
382
|
+
* Hides the outlines and hands back how to restore them.
|
|
383
|
+
*
|
|
384
|
+
* For screenshots: the outlines are fixed-position divs on the body, so they would be
|
|
385
|
+
* captured over the very text being photographed.
|
|
386
|
+
*/
|
|
387
|
+
hideOutlines() {
|
|
388
|
+
const hidden = [...registry.values()].map(entry => entry.outline).filter((box) => Boolean(box));
|
|
389
|
+
for (const box of hidden)
|
|
390
|
+
box.style.visibility = 'hidden';
|
|
391
|
+
return () => {
|
|
392
|
+
for (const box of hidden)
|
|
393
|
+
box.style.visibility = '';
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
/** Test seam. */
|
|
397
|
+
registeredCount: () => registry.size,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/** Undefined on either side means "any", which is how the dialog asks for everything. */
|
|
401
|
+
function matches(found, key, ns) {
|
|
402
|
+
if (key !== undefined && found.key !== key)
|
|
403
|
+
return false;
|
|
404
|
+
if (ns !== undefined && (found.ns ?? '') !== ns)
|
|
405
|
+
return false;
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=observer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../src/observer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying a key inside the string it produced.
|
|
3
|
+
*
|
|
4
|
+
* The problem: a click lands on a DOM node, and nothing about that node says which
|
|
5
|
+
* translation key rendered it. The string may have been interpolated into a text node,
|
|
6
|
+
* concatenated with another, put in a `title` attribute, or handed to a chart library - 938
|
|
7
|
+
* call sites in these applications do the last one, and no amount of framework integration
|
|
8
|
+
* can follow a string somebody put in a canvas.
|
|
9
|
+
*
|
|
10
|
+
* So the key travels WITH the string, written in characters that occupy no space: a zero-width
|
|
11
|
+
* non-joiner for 0 and a zero-width joiner for 1. Appended to the rendered text, they are
|
|
12
|
+
* invisible, they survive concatenation and interpolation, and they can be read back out of
|
|
13
|
+
* `textContent` wherever the string ended up.
|
|
14
|
+
*
|
|
15
|
+
* Two things make that practical rather than merely clever:
|
|
16
|
+
*
|
|
17
|
+
* - Keys are INTERNED. Encoding `{"k":"appointment_payment_amount_mismatch","n":"reception"}`
|
|
18
|
+
* would add ~500 invisible characters to a string of thirty, and the DOM would be mostly
|
|
19
|
+
* marks. Instead each distinct key gets a number, and only the number is encoded - one to
|
|
20
|
+
* three digits. The table lives in this module for the life of the page, which is exactly
|
|
21
|
+
* as long as the marks in the DOM do.
|
|
22
|
+
* - Each byte is written as NINE characters, eight bits and a trailing zero. That makes an
|
|
23
|
+
* encoded run a multiple of nine, which is how a run of marks is told apart from the odd
|
|
24
|
+
* zero-width character that turns up in real text (Persian and Hindi both use them, and
|
|
25
|
+
* emoji sequences are full of joiners).
|
|
26
|
+
*/
|
|
27
|
+
/** 0 and 1. Zero-width, so they render as nothing at all. */
|
|
28
|
+
const ZERO = '';
|
|
29
|
+
const ONE = '';
|
|
30
|
+
/** Between two encoded numbers, when two marked strings ended up in one node. */
|
|
31
|
+
const SEPARATOR = '\n';
|
|
32
|
+
const BITS_PER_BYTE = 9;
|
|
33
|
+
const RUN = new RegExp(`(?:[${ZERO}${ONE}]{${BITS_PER_BYTE}})+`, 'g');
|
|
34
|
+
/**
|
|
35
|
+
* Keys seen this page, so only an index has to be encoded.
|
|
36
|
+
*
|
|
37
|
+
* Module-level and never pruned, which is right: an entry costs a string, and a mark in the
|
|
38
|
+
* DOM is only meaningful while the page that wrote it is still open.
|
|
39
|
+
*/
|
|
40
|
+
const interned = [];
|
|
41
|
+
function intern(value) {
|
|
42
|
+
const existing = interned.indexOf(value);
|
|
43
|
+
if (existing !== -1)
|
|
44
|
+
return existing;
|
|
45
|
+
interned.push(value);
|
|
46
|
+
return interned.length - 1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* ASCII only, deliberately.
|
|
50
|
+
*
|
|
51
|
+
* What gets encoded is never the key itself - it is an index and a separator, so digits and
|
|
52
|
+
* one newline. That means no TextEncoder, which jsdom does not always provide, and no
|
|
53
|
+
* multi-byte handling to get wrong. The key's own characters live in the interning table,
|
|
54
|
+
* where they are just a JavaScript string.
|
|
55
|
+
*/
|
|
56
|
+
function encodeText(text) {
|
|
57
|
+
let bits = '';
|
|
58
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
59
|
+
bits += (text.charCodeAt(at) & 0x7f).toString(2).padStart(8, '0') + '0';
|
|
60
|
+
}
|
|
61
|
+
return [...bits].map(bit => (bit === '1' ? ONE : ZERO)).join('');
|
|
62
|
+
}
|
|
63
|
+
function decodeText(marks) {
|
|
64
|
+
let text = '';
|
|
65
|
+
for (let at = 0; at + BITS_PER_BYTE <= marks.length; at += BITS_PER_BYTE) {
|
|
66
|
+
const bits = [...marks.slice(at, at + 8)].map(character => (character === ONE ? '1' : '0')).join('');
|
|
67
|
+
text += String.fromCharCode(parseInt(bits, 2));
|
|
68
|
+
}
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
/** What a marked string carries: an index into the table, or several. */
|
|
72
|
+
export function mark(text, key) {
|
|
73
|
+
return text + encodeText(String(intern(JSON.stringify([key.key, key.ns ?? '', key.defaultValue ?? '']))) + SEPARATOR);
|
|
74
|
+
}
|
|
75
|
+
/** Whether a string is worth decoding, cheaply enough to run on every text node. */
|
|
76
|
+
export function isMarked(text) {
|
|
77
|
+
return text.includes(ZERO) || text.includes(ONE);
|
|
78
|
+
}
|
|
79
|
+
/** The text as a person sees it, with the marks taken out. */
|
|
80
|
+
export function unmark(text) {
|
|
81
|
+
return text.replace(RUN, '');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The keys a string carries, and the string without them.
|
|
85
|
+
*
|
|
86
|
+
* Anything that does not decode to a known index is ignored rather than reported: real text
|
|
87
|
+
* contains zero-width characters for its own reasons, and a run that happens to be a multiple
|
|
88
|
+
* of nine is a coincidence this must survive rather than a message.
|
|
89
|
+
*/
|
|
90
|
+
export function readMarks(text) {
|
|
91
|
+
const keys = [];
|
|
92
|
+
for (const run of text.match(RUN) ?? []) {
|
|
93
|
+
for (const encoded of decodeText(run).split(SEPARATOR)) {
|
|
94
|
+
if (!encoded)
|
|
95
|
+
continue;
|
|
96
|
+
const index = Number(encoded);
|
|
97
|
+
if (!Number.isInteger(index) || index < 0 || index >= interned.length)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const [key, ns, defaultValue] = JSON.parse(interned[index]);
|
|
101
|
+
keys.push({ key, ns: ns || undefined, defaultValue: defaultValue || undefined });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// An entry this module wrote cannot fail to parse; if it somehow does, the
|
|
105
|
+
// right answer is one fewer key rather than a broken page.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { text: unmark(text), keys };
|
|
110
|
+
}
|
|
111
|
+
/** Test seam: the table is a module global by design, and lives as long as the page. */
|
|
112
|
+
export function forgetInterned() {
|
|
113
|
+
interned.length = 0;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=marks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marks.js","sourceRoot":"","sources":["../src/marks.ts"],"names":[],"mappings":"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"}
|