@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.
@@ -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,3 @@
1
+ export { createObserver } from './observer.js';
2
+ export { mark, unmark, isMarked, readMarks, forgetInterned } from './marks.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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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"}