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