text-sanctifier 1.3.1 → 1.3.3

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,120 @@
1
+ // src/hiddenText.js
2
+ //
3
+ // Finds runs of the characters used to hide instructions in text a model reads:
4
+ // tag characters, variation selectors and invisible operators. Tag runs are decoded
5
+ // to the ASCII they spell. Used by `text-sanctifier check --ai-config`, and exported
6
+ // as `text-sanctifier/hidden`.
7
+ //
8
+ // Plain JavaScript with no Node APIs, so it also runs in a browser. src/index.js does
9
+ // not import this file, so it stays out of the browser bundle and its size budget.
10
+
11
+ /** @typedef {'tag' | 'variation_selector' | 'invisible_operator'} HiddenRunKind */
12
+
13
+ /**
14
+ * @typedef {Object} HiddenRun
15
+ * @property {HiddenRunKind} kind
16
+ * @property {number} offset - UTF-16 index of the first character
17
+ * @property {number} end - UTF-16 index just past the last character
18
+ * @property {number} count - number of code points in the run
19
+ * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText)
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} HiddenTextScan
24
+ * @property {HiddenRun[]} runs - runs that may hide text, in order
25
+ * @property {Array<[number, number]>} benign - [offset, end) spans of legitimate
26
+ * sequences (subdivision flags, a single emoji or text presentation selector)
27
+ */
28
+
29
+ const TAG_FIRST = 0xE0000;
30
+ const TAG_LAST = 0xE007F;
31
+ const CANCEL_TAG = 0xE007F;
32
+ const BLACK_FLAG = 0x1F3F4;
33
+
34
+ // RGI subdivision flags: U+1F3F4, the tag letters, U+E007F
35
+ const SUBDIVISION_FLAGS = new Set(['gbeng', 'gbsct', 'gbwls']);
36
+
37
+ /**
38
+ * @param {number} cp
39
+ * @returns {HiddenRunKind | null}
40
+ */
41
+ function kindOf(cp) {
42
+ if (cp >= TAG_FIRST && cp <= TAG_LAST) return 'tag';
43
+ if ((cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF) ||
44
+ (cp >= 0x180B && cp <= 0x180D) || cp === 0x180F) return 'variation_selector';
45
+ if (cp >= 0x2061 && cp <= 0x2064) return 'invisible_operator';
46
+ return null;
47
+ }
48
+
49
+ /** @param {number} cp */
50
+ const hex = (cp) => 'U+' + cp.toString(16).toUpperCase().padStart(4, '0');
51
+
52
+ /**
53
+ * Decodes tag characters to the ASCII they shadow: U+E0020–U+E007E become
54
+ * U+0020–U+007E. Anything else (U+E0001 LANGUAGE TAG, U+E007F CANCEL TAG, unassigned
55
+ * tags, other characters) is written as `<U+XXXX>`, so the result is printable.
56
+ * @param {string} text
57
+ * @returns {string}
58
+ */
59
+ export function decodeTagText(text) {
60
+ let out = '';
61
+ for (const ch of text) {
62
+ const cp = /** @type {number} */ (ch.codePointAt(0));
63
+ out += cp >= 0xE0020 && cp <= 0xE007E ? String.fromCharCode(cp - TAG_FIRST) : `<${hex(cp)}>`;
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * Finds runs of tag characters, variation selectors and invisible operators.
70
+ * Consecutive characters of the same kind form one run. Two kinds of sequence are
71
+ * legitimate and listed under `benign` instead of `runs`:
72
+ * - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
73
+ * tags for "gbeng", "gbsct" or "gbwls" and U+E007F
74
+ * - a single U+FE0E or U+FE0F (text or emoji presentation) after another character
75
+ * @param {string} text
76
+ * @returns {HiddenTextScan}
77
+ */
78
+ export function findHiddenRuns(text) {
79
+ /** @type {HiddenRun[]} */
80
+ const runs = [];
81
+ /** @type {Array<[number, number]>} */
82
+ const benign = [];
83
+
84
+ let i = 0;
85
+ while (i < text.length) {
86
+ const cp = /** @type {number} */ (text.codePointAt(i));
87
+ const kind = kindOf(cp);
88
+ if (kind === null) { i += cp > 0xFFFF ? 2 : 1; continue; }
89
+
90
+ const offset = i;
91
+ let count = 0;
92
+ let last = cp;
93
+ while (i < text.length) {
94
+ const c = /** @type {number} */ (text.codePointAt(i));
95
+ if (kindOf(c) !== kind) break;
96
+ last = c;
97
+ count++;
98
+ i += c > 0xFFFF ? 2 : 1;
99
+ }
100
+ const end = i;
101
+
102
+ if (kind === 'tag') {
103
+ const body = text.slice(offset, end);
104
+ const afterFlag = offset >= 2 && text.codePointAt(offset - 2) === BLACK_FLAG;
105
+ if (afterFlag && last === CANCEL_TAG && SUBDIVISION_FLAGS.has(decodeTagText(body).slice(0, -'<U+E007F>'.length))) {
106
+ benign.push([offset, end]);
107
+ } else {
108
+ runs.push({ kind, offset, end, count, decoded: decodeTagText(body) });
109
+ }
110
+ continue;
111
+ }
112
+ if (kind === 'variation_selector' && count === 1 && (cp === 0xFE0E || cp === 0xFE0F) &&
113
+ offset > 0 && !/\s/.test(text[offset - 1])) {
114
+ benign.push([offset, end]);
115
+ continue;
116
+ }
117
+ runs.push({ kind, offset, end, count });
118
+ }
119
+ return { runs, benign };
120
+ }