@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/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forgetInterned = exports.readMarks = exports.isMarked = exports.unmark = exports.mark = exports.createObserver = void 0;
|
|
4
|
+
var observer_js_1 = require("./observer.js");
|
|
5
|
+
Object.defineProperty(exports, "createObserver", { enumerable: true, get: function () { return observer_js_1.createObserver; } });
|
|
6
|
+
var marks_js_1 = require("./marks.js");
|
|
7
|
+
Object.defineProperty(exports, "mark", { enumerable: true, get: function () { return marks_js_1.mark; } });
|
|
8
|
+
Object.defineProperty(exports, "unmark", { enumerable: true, get: function () { return marks_js_1.unmark; } });
|
|
9
|
+
Object.defineProperty(exports, "isMarked", { enumerable: true, get: function () { return marks_js_1.isMarked; } });
|
|
10
|
+
Object.defineProperty(exports, "readMarks", { enumerable: true, get: function () { return marks_js_1.readMarks; } });
|
|
11
|
+
Object.defineProperty(exports, "forgetInterned", { enumerable: true, get: function () { return marks_js_1.forgetInterned; } });
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAA+C;AAAtC,6GAAA,cAAc,OAAA;AAEvB,uCAA+E;AAAtE,gGAAA,IAAI,OAAA;AAAE,kGAAA,MAAM,OAAA;AAAE,oGAAA,QAAQ,OAAA;AAAE,qGAAA,SAAS,OAAA;AAAE,0GAAA,cAAc,OAAA"}
|
package/dist/marks.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
export type MarkedKey = {
|
|
28
|
+
key: string;
|
|
29
|
+
ns?: string;
|
|
30
|
+
defaultValue?: string;
|
|
31
|
+
};
|
|
32
|
+
/** What a marked string carries: an index into the table, or several. */
|
|
33
|
+
export declare function mark(text: string, key: MarkedKey): string;
|
|
34
|
+
/** Whether a string is worth decoding, cheaply enough to run on every text node. */
|
|
35
|
+
export declare function isMarked(text: string): boolean;
|
|
36
|
+
/** The text as a person sees it, with the marks taken out. */
|
|
37
|
+
export declare function unmark(text: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* The keys a string carries, and the string without them.
|
|
40
|
+
*
|
|
41
|
+
* Anything that does not decode to a known index is ignored rather than reported: real text
|
|
42
|
+
* contains zero-width characters for its own reasons, and a run that happens to be a multiple
|
|
43
|
+
* of nine is a coincidence this must survive rather than a message.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readMarks(text: string): {
|
|
46
|
+
text: string;
|
|
47
|
+
keys: MarkedKey[];
|
|
48
|
+
};
|
|
49
|
+
/** Test seam: the table is a module global by design, and lives as long as the page. */
|
|
50
|
+
export declare function forgetInterned(): void;
|
package/dist/marks.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Carrying a key inside the string it produced.
|
|
4
|
+
*
|
|
5
|
+
* The problem: a click lands on a DOM node, and nothing about that node says which
|
|
6
|
+
* translation key rendered it. The string may have been interpolated into a text node,
|
|
7
|
+
* concatenated with another, put in a `title` attribute, or handed to a chart library - 938
|
|
8
|
+
* call sites in these applications do the last one, and no amount of framework integration
|
|
9
|
+
* can follow a string somebody put in a canvas.
|
|
10
|
+
*
|
|
11
|
+
* So the key travels WITH the string, written in characters that occupy no space: a zero-width
|
|
12
|
+
* non-joiner for 0 and a zero-width joiner for 1. Appended to the rendered text, they are
|
|
13
|
+
* invisible, they survive concatenation and interpolation, and they can be read back out of
|
|
14
|
+
* `textContent` wherever the string ended up.
|
|
15
|
+
*
|
|
16
|
+
* Two things make that practical rather than merely clever:
|
|
17
|
+
*
|
|
18
|
+
* - Keys are INTERNED. Encoding `{"k":"appointment_payment_amount_mismatch","n":"reception"}`
|
|
19
|
+
* would add ~500 invisible characters to a string of thirty, and the DOM would be mostly
|
|
20
|
+
* marks. Instead each distinct key gets a number, and only the number is encoded - one to
|
|
21
|
+
* three digits. The table lives in this module for the life of the page, which is exactly
|
|
22
|
+
* as long as the marks in the DOM do.
|
|
23
|
+
* - Each byte is written as NINE characters, eight bits and a trailing zero. That makes an
|
|
24
|
+
* encoded run a multiple of nine, which is how a run of marks is told apart from the odd
|
|
25
|
+
* zero-width character that turns up in real text (Persian and Hindi both use them, and
|
|
26
|
+
* emoji sequences are full of joiners).
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.mark = mark;
|
|
30
|
+
exports.isMarked = isMarked;
|
|
31
|
+
exports.unmark = unmark;
|
|
32
|
+
exports.readMarks = readMarks;
|
|
33
|
+
exports.forgetInterned = forgetInterned;
|
|
34
|
+
/** 0 and 1. Zero-width, so they render as nothing at all. */
|
|
35
|
+
const ZERO = '';
|
|
36
|
+
const ONE = '';
|
|
37
|
+
/** Between two encoded numbers, when two marked strings ended up in one node. */
|
|
38
|
+
const SEPARATOR = '\n';
|
|
39
|
+
const BITS_PER_BYTE = 9;
|
|
40
|
+
const RUN = new RegExp(`(?:[${ZERO}${ONE}]{${BITS_PER_BYTE}})+`, 'g');
|
|
41
|
+
/**
|
|
42
|
+
* Keys seen this page, so only an index has to be encoded.
|
|
43
|
+
*
|
|
44
|
+
* Module-level and never pruned, which is right: an entry costs a string, and a mark in the
|
|
45
|
+
* DOM is only meaningful while the page that wrote it is still open.
|
|
46
|
+
*/
|
|
47
|
+
const interned = [];
|
|
48
|
+
function intern(value) {
|
|
49
|
+
const existing = interned.indexOf(value);
|
|
50
|
+
if (existing !== -1)
|
|
51
|
+
return existing;
|
|
52
|
+
interned.push(value);
|
|
53
|
+
return interned.length - 1;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ASCII only, deliberately.
|
|
57
|
+
*
|
|
58
|
+
* What gets encoded is never the key itself - it is an index and a separator, so digits and
|
|
59
|
+
* one newline. That means no TextEncoder, which jsdom does not always provide, and no
|
|
60
|
+
* multi-byte handling to get wrong. The key's own characters live in the interning table,
|
|
61
|
+
* where they are just a JavaScript string.
|
|
62
|
+
*/
|
|
63
|
+
function encodeText(text) {
|
|
64
|
+
let bits = '';
|
|
65
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
66
|
+
bits += (text.charCodeAt(at) & 0x7f).toString(2).padStart(8, '0') + '0';
|
|
67
|
+
}
|
|
68
|
+
return [...bits].map(bit => (bit === '1' ? ONE : ZERO)).join('');
|
|
69
|
+
}
|
|
70
|
+
function decodeText(marks) {
|
|
71
|
+
let text = '';
|
|
72
|
+
for (let at = 0; at + BITS_PER_BYTE <= marks.length; at += BITS_PER_BYTE) {
|
|
73
|
+
const bits = [...marks.slice(at, at + 8)].map(character => (character === ONE ? '1' : '0')).join('');
|
|
74
|
+
text += String.fromCharCode(parseInt(bits, 2));
|
|
75
|
+
}
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
/** What a marked string carries: an index into the table, or several. */
|
|
79
|
+
function mark(text, key) {
|
|
80
|
+
return text + encodeText(String(intern(JSON.stringify([key.key, key.ns ?? '', key.defaultValue ?? '']))) + SEPARATOR);
|
|
81
|
+
}
|
|
82
|
+
/** Whether a string is worth decoding, cheaply enough to run on every text node. */
|
|
83
|
+
function isMarked(text) {
|
|
84
|
+
return text.includes(ZERO) || text.includes(ONE);
|
|
85
|
+
}
|
|
86
|
+
/** The text as a person sees it, with the marks taken out. */
|
|
87
|
+
function unmark(text) {
|
|
88
|
+
return text.replace(RUN, '');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The keys a string carries, and the string without them.
|
|
92
|
+
*
|
|
93
|
+
* Anything that does not decode to a known index is ignored rather than reported: real text
|
|
94
|
+
* contains zero-width characters for its own reasons, and a run that happens to be a multiple
|
|
95
|
+
* of nine is a coincidence this must survive rather than a message.
|
|
96
|
+
*/
|
|
97
|
+
function readMarks(text) {
|
|
98
|
+
const keys = [];
|
|
99
|
+
for (const run of text.match(RUN) ?? []) {
|
|
100
|
+
for (const encoded of decodeText(run).split(SEPARATOR)) {
|
|
101
|
+
if (!encoded)
|
|
102
|
+
continue;
|
|
103
|
+
const index = Number(encoded);
|
|
104
|
+
if (!Number.isInteger(index) || index < 0 || index >= interned.length)
|
|
105
|
+
continue;
|
|
106
|
+
try {
|
|
107
|
+
const [key, ns, defaultValue] = JSON.parse(interned[index]);
|
|
108
|
+
keys.push({ key, ns: ns || undefined, defaultValue: defaultValue || undefined });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// An entry this module wrote cannot fail to parse; if it somehow does, the
|
|
112
|
+
// right answer is one fewer key rather than a broken page.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { text: unmark(text), keys };
|
|
117
|
+
}
|
|
118
|
+
/** Test seam: the table is a module global by design, and lives as long as the page. */
|
|
119
|
+
function forgetInterned() {
|
|
120
|
+
interned.length = 0;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=marks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marks.js","sourceRoot":"","sources":["../src/marks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TranslateProps } from '@worthy-ventures/metaglotta-runtime';
|
|
2
|
+
import { type MarkedKey } from './marks.js';
|
|
3
|
+
/**
|
|
4
|
+
* Watches the page for marked text, and turns a click on it back into a key.
|
|
5
|
+
*
|
|
6
|
+
* Three jobs, and they are separate on purpose:
|
|
7
|
+
*
|
|
8
|
+
* 1. MARK - every string the runtime produces gets its key appended in invisible characters.
|
|
9
|
+
* This is the `decorate` hook, and it is the only thing that touches the runtime.
|
|
10
|
+
* 2. SCAN - as marked text arrives in the DOM, read the keys out, take the marks back OUT of
|
|
11
|
+
* the node so nothing can copy them into a clipboard, and remember which element the keys
|
|
12
|
+
* belong to.
|
|
13
|
+
* 3. POINT - while the modifier is held, outline whatever is under the cursor and swallow the
|
|
14
|
+
* click, so ALT+clicking a "Delete" button opens the dialog rather than deleting anything.
|
|
15
|
+
*
|
|
16
|
+
* Authoring only. Nothing in a production build should ever import this.
|
|
17
|
+
*/
|
|
18
|
+
export type ModifierName = 'Alt' | 'Control' | 'Shift' | 'Meta';
|
|
19
|
+
export type KeyPosition = {
|
|
20
|
+
key: string;
|
|
21
|
+
ns: string;
|
|
22
|
+
position: {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type ObserverOptions = {
|
|
30
|
+
/** What to do with an armed click. The target is the element the keys were found on. */
|
|
31
|
+
onClick: (keys: MarkedKey[], target: HTMLElement) => void;
|
|
32
|
+
/** Held to arm it. All of them, if more than one. Default: Alt. */
|
|
33
|
+
keys?: ModifierName[];
|
|
34
|
+
/** Attributes whose value may be a translation. */
|
|
35
|
+
attributes?: string[];
|
|
36
|
+
/** Where to watch. Default: the whole document. */
|
|
37
|
+
root?: Element | Document;
|
|
38
|
+
/** A subtree to leave alone entirely - the editing dialog's own, for one. */
|
|
39
|
+
ignore?: (element: Element) => boolean;
|
|
40
|
+
highlightColor?: string;
|
|
41
|
+
highlightWidth?: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function createObserver(options: ObserverOptions): {
|
|
44
|
+
/**
|
|
45
|
+
* Pass as `decorate` to the runtime. Everything else follows from this.
|
|
46
|
+
*
|
|
47
|
+
* The namespace comes from the runtime, not from `props`: props hold what the caller
|
|
48
|
+
* asked with, which for most templates is nothing at all, while this is where the
|
|
49
|
+
* string was actually resolved from. Marking with the former is what made the dialog
|
|
50
|
+
* open on the default namespace with nothing in it.
|
|
51
|
+
*/
|
|
52
|
+
mark: (result: string, props: TranslateProps, namespace: string) => string;
|
|
53
|
+
run(): void;
|
|
54
|
+
stop(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Where a key is on screen, in viewport coordinates.
|
|
57
|
+
*
|
|
58
|
+
* This is what a screenshot's boxes are drawn from, so the order matters: sorted by
|
|
59
|
+
* position in the document, because the dialog labels them in the order it gets them
|
|
60
|
+
* and a reader expects the first box to be the first occurrence.
|
|
61
|
+
*/
|
|
62
|
+
findPositions(key?: string, ns?: string): KeyPosition[];
|
|
63
|
+
/** Outlines every place a key appears. Returns how to put it back. */
|
|
64
|
+
highlight(key?: string, ns?: string): {
|
|
65
|
+
unhighlight: () => void;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Hides the outlines and hands back how to restore them.
|
|
69
|
+
*
|
|
70
|
+
* For screenshots: the outlines are fixed-position divs on the body, so they would be
|
|
71
|
+
* captured over the very text being photographed.
|
|
72
|
+
*/
|
|
73
|
+
hideOutlines(): () => void;
|
|
74
|
+
/** Test seam. */
|
|
75
|
+
registeredCount: () => number;
|
|
76
|
+
};
|
|
77
|
+
export type Observer = ReturnType<typeof createObserver>;
|