text-sanctifier 1.3.8 → 1.3.10

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/robots.txt CHANGED
@@ -1,79 +1,79 @@
1
- # WATT3D — AI scraping policy
2
- #
3
- # This file enumerates known AI / LLM training and inference crawlers
4
- # that the WATT3D AI Training Rider does not consent to. See
5
- # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
- #
7
- # Honoring robots.txt is voluntary. The rider applies whether or not
8
- # a bot respects this file. The enumeration is non-exhaustive — bots
9
- # not listed here are still bound by the rider if they perform
10
- # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
-
12
- User-agent: GPTBot
13
- Disallow: /
14
-
15
- User-agent: ChatGPT-User
16
- Disallow: /
17
-
18
- User-agent: OAI-SearchBot
19
- Disallow: /
20
-
21
- User-agent: anthropic-ai
22
- Disallow: /
23
-
24
- User-agent: ClaudeBot
25
- Disallow: /
26
-
27
- User-agent: Claude-Web
28
- Disallow: /
29
-
30
- User-agent: Claude-SearchBot
31
- Disallow: /
32
-
33
- User-agent: Google-Extended
34
- Disallow: /
35
-
36
- User-agent: Applebot-Extended
37
- Disallow: /
38
-
39
- User-agent: Meta-ExternalAgent
40
- Disallow: /
41
-
42
- User-agent: Meta-ExternalFetcher
43
- Disallow: /
44
-
45
- User-agent: FacebookBot
46
- Disallow: /
47
-
48
- User-agent: CCBot
49
- Disallow: /
50
-
51
- User-agent: PerplexityBot
52
- Disallow: /
53
-
54
- User-agent: Perplexity-User
55
- Disallow: /
56
-
57
- User-agent: cohere-ai
58
- Disallow: /
59
-
60
- User-agent: Bytespider
61
- Disallow: /
62
-
63
- User-agent: Amazonbot
64
- Disallow: /
65
-
66
- User-agent: Omgilibot
67
- Disallow: /
68
-
69
- User-agent: AI2Bot
70
- Disallow: /
71
-
72
- User-agent: Diffbot
73
- Disallow: /
74
-
75
- # Everyone else: full access. Search engines and human-driven crawlers
76
- # remain welcome.
77
-
78
- User-agent: *
79
- Allow: /
1
+ # WATT3D — AI scraping policy
2
+ #
3
+ # This file enumerates known AI / LLM training and inference crawlers
4
+ # that the WATT3D AI Training Rider does not consent to. See
5
+ # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
+ #
7
+ # Honoring robots.txt is voluntary. The rider applies whether or not
8
+ # a bot respects this file. The enumeration is non-exhaustive — bots
9
+ # not listed here are still bound by the rider if they perform
10
+ # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
+
12
+ User-agent: GPTBot
13
+ Disallow: /
14
+
15
+ User-agent: ChatGPT-User
16
+ Disallow: /
17
+
18
+ User-agent: OAI-SearchBot
19
+ Disallow: /
20
+
21
+ User-agent: anthropic-ai
22
+ Disallow: /
23
+
24
+ User-agent: ClaudeBot
25
+ Disallow: /
26
+
27
+ User-agent: Claude-Web
28
+ Disallow: /
29
+
30
+ User-agent: Claude-SearchBot
31
+ Disallow: /
32
+
33
+ User-agent: Google-Extended
34
+ Disallow: /
35
+
36
+ User-agent: Applebot-Extended
37
+ Disallow: /
38
+
39
+ User-agent: Meta-ExternalAgent
40
+ Disallow: /
41
+
42
+ User-agent: Meta-ExternalFetcher
43
+ Disallow: /
44
+
45
+ User-agent: FacebookBot
46
+ Disallow: /
47
+
48
+ User-agent: CCBot
49
+ Disallow: /
50
+
51
+ User-agent: PerplexityBot
52
+ Disallow: /
53
+
54
+ User-agent: Perplexity-User
55
+ Disallow: /
56
+
57
+ User-agent: cohere-ai
58
+ Disallow: /
59
+
60
+ User-agent: Bytespider
61
+ Disallow: /
62
+
63
+ User-agent: Amazonbot
64
+ Disallow: /
65
+
66
+ User-agent: Omgilibot
67
+ Disallow: /
68
+
69
+ User-agent: AI2Bot
70
+ Disallow: /
71
+
72
+ User-agent: Diffbot
73
+ Disallow: /
74
+
75
+ # Everyone else: full access. Search engines and human-driven crawlers
76
+ # remain welcome.
77
+
78
+ User-agent: *
79
+ Allow: /
@@ -0,0 +1,171 @@
1
+ // Internal hidden-span scanner; not a package entry point.
2
+
3
+ /**
4
+ * @typedef {import('./hiddenText.js').HiddenRunKind} HiddenRunKind
5
+ * @typedef {import('./hiddenText.js').HiddenRun} HiddenRun
6
+ * @typedef {import('./hiddenText.js').HiddenRunCounts} HiddenRunCounts
7
+ * @typedef {import('./hiddenText.js').HiddenTextScan} HiddenTextScan
8
+ * @typedef {import('./hiddenText.js').HiddenScanOptions} HiddenScanOptions
9
+ */
10
+
11
+ const TAG_FIRST = 0xE0000;
12
+ const TAG_LAST = 0xE007F;
13
+ const CANCEL_TAG = 0xE007F;
14
+ const BLACK_FLAG = 0x1F3F4;
15
+
16
+ // RGI subdivision flags: U+1F3F4, the tag letters, U+E007F
17
+ const SUBDIVISION_FLAGS = new Set(['gbeng', 'gbsct', 'gbwls']);
18
+
19
+ /**
20
+ * @param {number} cp
21
+ * @returns {HiddenRunKind | null}
22
+ */
23
+ function kindOf(cp) {
24
+ if (cp >= TAG_FIRST && cp <= TAG_LAST) return 'tag';
25
+ if ((cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF) ||
26
+ (cp >= 0x180B && cp <= 0x180D) || cp === 0x180F) return 'variation_selector';
27
+ if (cp >= 0x2061 && cp <= 0x2064) return 'invisible_operator';
28
+ // Unassigned and default ignorable: U+E0080-U+E00FF and U+E01F0-U+E0FFF
29
+ if (cp >= 0xE0080 && cp <= 0xE0FFF) return 'reserved_tag_plane';
30
+ return null;
31
+ }
32
+
33
+ /** @param {number} cp */
34
+ const hex = (cp) => 'U+' + cp.toString(16).toUpperCase().padStart(4, '0');
35
+
36
+ /**
37
+ * Decodes text[start, end) as decodeTagText does, stopping before the output would
38
+ * pass `limit` UTF-16 units. A `<U+XXXX>` marker is written whole or not at all.
39
+ * @param {string} text
40
+ * @param {number} start
41
+ * @param {number} end
42
+ * @param {number} limit
43
+ * @returns {{ decoded: string, truncated: boolean }}
44
+ */
45
+ export function decodeRange(text, start, end, limit) {
46
+ let out = '';
47
+ let i = start;
48
+ while (i < end) {
49
+ const cp = /** @type {number} */ (text.codePointAt(i));
50
+ const piece = cp >= 0xE0020 && cp <= 0xE007E ? String.fromCharCode(cp - TAG_FIRST) : `<${hex(cp)}>`;
51
+ if (out.length + piece.length > limit) return { decoded: out, truncated: true };
52
+ out += piece;
53
+ i += cp > 0xFFFF ? 2 : 1;
54
+ }
55
+ return { decoded: out, truncated: false };
56
+ }
57
+
58
+ // Tested at an index of the text itself, so the check allocates nothing.
59
+ const PICTOGRAPHIC_AT = /\p{Extended_Pictographic}/yu;
60
+
61
+ /**
62
+ * True when a presentation selector at `index` follows an emoji-capable character:
63
+ * an Extended_Pictographic code point, or a keycap base (#, * or a digit) with U+20E3 after.
64
+ * @param {string} text
65
+ * @param {number} index
66
+ * @returns {boolean}
67
+ */
68
+ function isPresentationBase(text, index) {
69
+ if (index === 0) return false;
70
+ const low = text.charCodeAt(index - 1);
71
+ const p = low >= 0xDC00 && low <= 0xDFFF && index >= 2 &&
72
+ (text.charCodeAt(index - 2) & 0xFC00) === 0xD800 ? index - 2 : index - 1;
73
+ const before = text.charCodeAt(p);
74
+ if ((before === 0x23 || before === 0x2A || (before >= 0x30 && before <= 0x39)) && text.charCodeAt(index + 1) === 0x20E3) return true;
75
+ PICTOGRAPHIC_AT.lastIndex = p;
76
+ return PICTOGRAPHIC_AT.test(text);
77
+ }
78
+
79
+ /**
80
+ * A cap option: a non-negative integer or Infinity; anything else means no cap.
81
+ * @param {unknown} value
82
+ * @returns {number}
83
+ */
84
+ function cap(value) {
85
+ return typeof value === 'number' && (value === Infinity || (Number.isInteger(value) && value >= 0))
86
+ ? value
87
+ : Infinity;
88
+ }
89
+
90
+ /**
91
+ * @param {string} text
92
+ * @param {HiddenScanOptions} [options]
93
+ * @param {(span: {offset: number, end: number, benign: boolean, kind: HiddenRunKind}) => void} [onSpan] - internal streaming visitor
94
+ * @returns {HiddenTextScan}
95
+ */
96
+ export function scanHiddenSpans(text, options = {}, onSpan) {
97
+ if (typeof text !== 'string') throw new TypeError('findHiddenRuns expects a string input.');
98
+ const maxRuns = cap(options && options.maxRuns);
99
+ const maxDecodedLength = cap(options && options.maxDecodedLength);
100
+
101
+ /** @type {HiddenRun[]} */
102
+ const runs = [];
103
+ /** @type {Array<[number, number]>} */
104
+ const benign = [];
105
+ /** @type {HiddenRunCounts} */
106
+ const counts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
107
+ /** @type {HiddenRunCounts} */
108
+ const codePointCounts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
109
+ let supplementarySelectorCount = 0;
110
+ let total = 0;
111
+ let benignTotal = 0;
112
+ let line = 1;
113
+ let lineStart = 0;
114
+
115
+ let i = 0;
116
+ while (i < text.length) {
117
+ const cp = /** @type {number} */ (text.codePointAt(i));
118
+ const kind = kindOf(cp);
119
+ if (kind === null) {
120
+ if (cp === 0x0A || (cp === 0x0D && text.charCodeAt(i + 1) !== 0x0A)) { // CRLF ends at its LF
121
+ line++;
122
+ lineStart = i + 1;
123
+ }
124
+ i += cp > 0xFFFF ? 2 : 1;
125
+ continue;
126
+ }
127
+
128
+ const offset = i;
129
+ let count = 0;
130
+ let supplementary = 0;
131
+ let last = cp;
132
+ while (i < text.length) {
133
+ const c = /** @type {number} */ (text.codePointAt(i));
134
+ if (kindOf(c) !== kind) break;
135
+ last = c;
136
+ count++;
137
+ if (c >= 0xE0100 && c <= 0xE01EF) supplementary++;
138
+ i += c > 0xFFFF ? 2 : 1;
139
+ }
140
+ const end = i;
141
+
142
+ // A subdivision flag is exactly five tag letters and CANCEL TAG after U+1F3F4.
143
+ const isBenign = kind === 'tag'
144
+ ? count === 6 && last === CANCEL_TAG && offset >= 2 && text.codePointAt(offset - 2) === BLACK_FLAG &&
145
+ SUBDIVISION_FLAGS.has(decodeRange(text, offset, end - 2, Infinity).decoded)
146
+ : kind === 'variation_selector' && count === 1 && (cp === 0xFE0E || cp === 0xFE0F) &&
147
+ isPresentationBase(text, offset);
148
+ if (onSpan) onSpan({ offset, end, benign: isBenign, kind });
149
+ if (isBenign) {
150
+ benignTotal++;
151
+ if (benign.length < maxRuns) benign.push([offset, end]);
152
+ continue;
153
+ }
154
+
155
+ counts[kind]++;
156
+ codePointCounts[kind] += count;
157
+ supplementarySelectorCount += supplementary;
158
+ total++;
159
+ if (runs.length < maxRuns) {
160
+ /** @type {HiddenRun} */
161
+ const run = { kind, offset, end, line, column: offset - lineStart + 1, count };
162
+ if (kind === 'tag') {
163
+ const { decoded, truncated } = decodeRange(text, offset, end, maxDecodedLength);
164
+ run.decoded = decoded;
165
+ run.decodedTruncated = truncated;
166
+ }
167
+ runs.push(run);
168
+ }
169
+ }
170
+ return { runs, benign, counts, codePointCounts, supplementarySelectorCount, total, benignTotal, truncated: total > runs.length || benignTotal > benign.length };
171
+ }
package/src/hiddenText.js CHANGED
@@ -1,233 +1,96 @@
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, invisible operators and the reserved code points
5
- // of the tag plane. Tag runs are decoded
6
- // to the ASCII they spell. Used by `text-sanctifier check --ai-config`, and exported
7
- // as `text-sanctifier/hidden`.
8
- //
9
- // Plain JavaScript with no Node APIs, so it also runs in a browser. src/index.js does
10
- // not import this file, so it stays out of the browser bundle and its size budget.
11
-
12
- export { VERSION } from './version.generated.js';
13
-
14
- /** @typedef {'tag' | 'variation_selector' | 'invisible_operator' | 'reserved_tag_plane'} HiddenRunKind */
15
-
16
- /**
17
- * @typedef {Object} HiddenRun
18
- * @property {HiddenRunKind} kind
19
- * @property {number} offset - UTF-16 index of the first character
20
- * @property {number} end - UTF-16 index just past the last character
21
- * @property {number} count - number of code points in the run
22
- * @property {number} line - 1-based line of the first character (as in inspectTextDetailed)
23
- * @property {number} column - 1-based column of the first character, in UTF-16 units
24
- * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText),
25
- * at most `maxDecodedLength` UTF-16 units
26
- * @property {boolean} [decodedTruncated] - tag runs only: true when `decoded` stops short of the run
27
- */
28
-
29
- /**
30
- * @typedef {Object} HiddenRunCounts
31
- * @property {number} tag
32
- * @property {number} variation_selector
33
- * @property {number} invisible_operator
34
- * @property {number} reserved_tag_plane
35
- */
36
-
37
- /**
38
- * @typedef {Object} HiddenTextScan
39
- * @property {HiddenRun[]} runs - runs that may hide text, in order, at most `maxRuns` long
40
- * @property {Array<[number, number]>} benign - [offset, end) spans of legitimate
41
- * sequences (subdivision flags, a single emoji or text presentation selector), at most `maxRuns` long
42
- * @property {HiddenRunCounts} counts - every run per kind, listed or not
43
- * @property {HiddenRunCounts} codePointCounts - code points in those runs per kind, listed or not
44
- * @property {number} supplementarySelectorCount - code points U+E0100–U+E01EF in those runs, which
45
- * (unlike the emoji selectors U+FE00–U+FE0F) can carry data; included in codePointCounts.variation_selector
46
- * @property {number} total - sum of `counts`
47
- * @property {number} benignTotal - every benign span, listed or not
48
- * @property {boolean} truncated - true when runs or benign spans were left out of the lists
49
- */
50
-
51
- /**
52
- * @typedef {Object} HiddenScanOptions
53
- * @property {number} [maxRuns=Infinity] - cap on listed runs, and separately on listed benign spans
54
- * (non-negative integer or Infinity). Counts stay exact.
55
- * @property {number} [maxDecodedLength=Infinity] - cap on each run's `decoded` text, in UTF-16 units.
56
- * For both, any other value (negative, fractional, NaN, a string) means the default, no cap.
57
- */
58
-
59
- const TAG_FIRST = 0xE0000;
60
- const TAG_LAST = 0xE007F;
61
- const CANCEL_TAG = 0xE007F;
62
- const BLACK_FLAG = 0x1F3F4;
63
-
64
- // RGI subdivision flags: U+1F3F4, the tag letters, U+E007F
65
- const SUBDIVISION_FLAGS = new Set(['gbeng', 'gbsct', 'gbwls']);
66
-
67
- /**
68
- * @param {number} cp
69
- * @returns {HiddenRunKind | null}
70
- */
71
- function kindOf(cp) {
72
- if (cp >= TAG_FIRST && cp <= TAG_LAST) return 'tag';
73
- if ((cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF) ||
74
- (cp >= 0x180B && cp <= 0x180D) || cp === 0x180F) return 'variation_selector';
75
- if (cp >= 0x2061 && cp <= 0x2064) return 'invisible_operator';
76
- // Unassigned and default ignorable: U+E0080-U+E00FF and U+E01F0-U+E0FFF
77
- if (cp >= 0xE0080 && cp <= 0xE0FFF) return 'reserved_tag_plane';
78
- return null;
79
- }
80
-
81
- /** @param {number} cp */
82
- const hex = (cp) => 'U+' + cp.toString(16).toUpperCase().padStart(4, '0');
83
-
84
- /**
85
- * Decodes tag characters to the ASCII they shadow: U+E0020–U+E007E become
86
- * U+0020–U+007E. Anything else (U+E0001 LANGUAGE TAG, U+E007F CANCEL TAG, unassigned
87
- * tags, other characters) is written as `<U+XXXX>`, so the result is printable.
88
- * @param {string} text
89
- * @returns {string}
90
- */
91
- export function decodeTagText(text) {
92
- return decodeRange(text, 0, text.length, Infinity).decoded;
93
- }
94
-
95
- /**
96
- * Decodes text[start, end) as decodeTagText does, stopping before the output would
97
- * pass `limit` UTF-16 units. A `<U+XXXX>` marker is written whole or not at all.
98
- * @param {string} text
99
- * @param {number} start
100
- * @param {number} end
101
- * @param {number} limit
102
- * @returns {{ decoded: string, truncated: boolean }}
103
- */
104
- function decodeRange(text, start, end, limit) {
105
- let out = '';
106
- let i = start;
107
- while (i < end) {
108
- const cp = /** @type {number} */ (text.codePointAt(i));
109
- const piece = cp >= 0xE0020 && cp <= 0xE007E ? String.fromCharCode(cp - TAG_FIRST) : `<${hex(cp)}>`;
110
- if (out.length + piece.length > limit) return { decoded: out, truncated: true };
111
- out += piece;
112
- i += cp > 0xFFFF ? 2 : 1;
113
- }
114
- return { decoded: out, truncated: false };
115
- }
116
-
117
- // Whitespace as JavaScript's \s defines it, tested at an index of the text itself,
118
- // so the check allocates nothing.
119
- const SPACE_AT = /\s/y;
120
-
121
- /**
122
- * @param {string} text
123
- * @param {number} index
124
- * @returns {boolean}
125
- */
126
- function isSpaceAt(text, index) {
127
- SPACE_AT.lastIndex = index;
128
- return SPACE_AT.test(text);
129
- }
130
-
131
- /**
132
- * A cap option: a non-negative integer or Infinity; anything else means no cap.
133
- * @param {unknown} value
134
- * @returns {number}
135
- */
136
- function cap(value) {
137
- return typeof value === 'number' && (value === Infinity || (Number.isInteger(value) && value >= 0))
138
- ? value
139
- : Infinity;
140
- }
141
-
142
- /**
143
- * Finds runs of tag characters, variation selectors, invisible operators and reserved
144
- * tag-plane code points (U+E0080–U+E00FF, U+E01F0–U+E0FFF).
145
- * Consecutive characters of the same kind form one run. Two kinds of sequence are
146
- * legitimate and listed under `benign` instead of `runs`:
147
- * - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
148
- * tags for "gbeng", "gbsct" or "gbwls" and U+E007F
149
- * - a single U+FE0E or U+FE0F (text or emoji presentation) after another character
150
- *
151
- * `line` and `column` are counted as in inspectTextDetailed. Every run and benign
152
- * span is counted; only the first `maxRuns` of each are listed. With the caps set,
153
- * memory stays flat however large the input is. One pass over the string, no copy.
154
- * Set both caps for untrusted input: without them, results grow with the input.
155
- *
156
- * @param {string} text
157
- * @param {HiddenScanOptions} [options]
158
- * @returns {HiddenTextScan}
159
- */
160
- export function findHiddenRuns(text, options = {}) {
161
- const maxRuns = cap(options && options.maxRuns);
162
- const maxDecodedLength = cap(options && options.maxDecodedLength);
163
-
164
- /** @type {HiddenRun[]} */
165
- const runs = [];
166
- /** @type {Array<[number, number]>} */
167
- const benign = [];
168
- /** @type {HiddenRunCounts} */
169
- const counts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
170
- /** @type {HiddenRunCounts} */
171
- const codePointCounts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
172
- let supplementarySelectorCount = 0;
173
- let total = 0;
174
- let benignTotal = 0;
175
- let line = 1;
176
- let lineStart = 0;
177
-
178
- let i = 0;
179
- while (i < text.length) {
180
- const cp = /** @type {number} */ (text.codePointAt(i));
181
- const kind = kindOf(cp);
182
- if (kind === null) {
183
- if (cp === 0x0A || (cp === 0x0D && text.charCodeAt(i + 1) !== 0x0A)) { // CRLF ends at its LF
184
- line++;
185
- lineStart = i + 1;
186
- }
187
- i += cp > 0xFFFF ? 2 : 1;
188
- continue;
189
- }
190
-
191
- const offset = i;
192
- let count = 0;
193
- let supplementary = 0;
194
- let last = cp;
195
- while (i < text.length) {
196
- const c = /** @type {number} */ (text.codePointAt(i));
197
- if (kindOf(c) !== kind) break;
198
- last = c;
199
- count++;
200
- if (c >= 0xE0100 && c <= 0xE01EF) supplementary++;
201
- i += c > 0xFFFF ? 2 : 1;
202
- }
203
- const end = i;
204
-
205
- // A subdivision flag is exactly five tag letters and CANCEL TAG after U+1F3F4.
206
- const isBenign = kind === 'tag'
207
- ? count === 6 && last === CANCEL_TAG && offset >= 2 && text.codePointAt(offset - 2) === BLACK_FLAG &&
208
- SUBDIVISION_FLAGS.has(decodeRange(text, offset, end - 2, Infinity).decoded)
209
- : kind === 'variation_selector' && count === 1 && (cp === 0xFE0E || cp === 0xFE0F) &&
210
- offset > 0 && !isSpaceAt(text, offset - 1);
211
- if (isBenign) {
212
- benignTotal++;
213
- if (benign.length < maxRuns) benign.push([offset, end]);
214
- continue;
215
- }
216
-
217
- counts[kind]++;
218
- codePointCounts[kind] += count;
219
- supplementarySelectorCount += supplementary;
220
- total++;
221
- if (runs.length < maxRuns) {
222
- /** @type {HiddenRun} */
223
- const run = { kind, offset, end, line, column: offset - lineStart + 1, count };
224
- if (kind === 'tag') {
225
- const { decoded, truncated } = decodeRange(text, offset, end, maxDecodedLength);
226
- run.decoded = decoded;
227
- run.decodedTruncated = truncated;
228
- }
229
- runs.push(run);
230
- }
231
- }
232
- return { runs, benign, counts, codePointCounts, supplementarySelectorCount, total, benignTotal, truncated: total > runs.length || benignTotal > benign.length };
233
- }
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, invisible operators and the reserved code points
5
+ // of the tag plane. Tag runs are decoded
6
+ // to the ASCII they spell. Used by `text-sanctifier check --ai-config`, and exported
7
+ // as `text-sanctifier/hidden`.
8
+ //
9
+ // Plain JavaScript with no Node APIs, so it also runs in a browser. src/index.js does
10
+ // not import this file, so it stays out of the browser bundle and its size budget.
11
+
12
+ import { scanHiddenSpans, decodeRange } from './hiddenScan.js';
13
+
14
+ export { VERSION } from './version.generated.js';
15
+
16
+ /** @typedef {'tag' | 'variation_selector' | 'invisible_operator' | 'reserved_tag_plane'} HiddenRunKind */
17
+
18
+ /**
19
+ * @typedef {Object} HiddenRun
20
+ * @property {HiddenRunKind} kind
21
+ * @property {number} offset - UTF-16 index of the first character
22
+ * @property {number} end - UTF-16 index just past the last character
23
+ * @property {number} count - number of code points in the run
24
+ * @property {number} line - 1-based line of the first character (as in inspectTextDetailed)
25
+ * @property {number} column - 1-based column of the first character, in UTF-16 units
26
+ * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText),
27
+ * at most `maxDecodedLength` UTF-16 units
28
+ * @property {boolean} [decodedTruncated] - tag runs only: true when `decoded` stops short of the run
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} HiddenRunCounts
33
+ * @property {number} tag
34
+ * @property {number} variation_selector
35
+ * @property {number} invisible_operator
36
+ * @property {number} reserved_tag_plane
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} HiddenTextScan
41
+ * @property {HiddenRun[]} runs - runs that may hide text, in order, at most `maxRuns` long
42
+ * @property {Array<[number, number]>} benign - [offset, end) spans of legitimate
43
+ * sequences (subdivision flags, a single emoji or text presentation selector), at most `maxRuns` long
44
+ * @property {HiddenRunCounts} counts - every run per kind, listed or not
45
+ * @property {HiddenRunCounts} codePointCounts - code points in those runs per kind, listed or not
46
+ * @property {number} supplementarySelectorCount - code points U+E0100–U+E01EF in those runs, which
47
+ * (unlike the emoji selectors U+FE00–U+FE0F) can carry data; included in codePointCounts.variation_selector
48
+ * @property {number} total - sum of `counts`
49
+ * @property {number} benignTotal - every benign span, listed or not
50
+ * @property {boolean} truncated - true when runs or benign spans were left out of the lists
51
+ */
52
+
53
+ /**
54
+ * @typedef {Object} HiddenScanOptions
55
+ * @property {number} [maxRuns=Infinity] - cap on listed runs, and separately on listed benign spans
56
+ * (non-negative integer or Infinity). Counts stay exact.
57
+ * @property {number} [maxDecodedLength=Infinity] - cap on each run's `decoded` text, in UTF-16 units.
58
+ * For both, any other value (negative, fractional, NaN, a string) means the default, no cap.
59
+ */
60
+
61
+ /**
62
+ * Decodes tag characters to the ASCII they shadow: U+E0020–U+E007E become
63
+ * U+0020–U+007E. Anything else (U+E0001 LANGUAGE TAG, U+E007F CANCEL TAG, unassigned
64
+ * tags, other characters) is written as `<U+XXXX>`, so the result is printable.
65
+ * @param {string} text
66
+ * @returns {string}
67
+ */
68
+ export function decodeTagText(text) {
69
+ if (typeof text !== 'string') throw new TypeError('decodeTagText expects a string input.');
70
+ return decodeRange(text, 0, text.length, Infinity).decoded;
71
+ }
72
+
73
+ /**
74
+ * Finds runs of tag characters, variation selectors, invisible operators and reserved
75
+ * tag-plane code points (U+E0080–U+E00FF, U+E01F0–U+E0FFF).
76
+ * Consecutive characters of the same kind form one run. Two kinds of sequence are
77
+ * legitimate and listed under `benign` instead of `runs`:
78
+ * - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
79
+ * tags for "gbeng", "gbsct" or "gbwls" and U+E007F
80
+ * - a single U+FE0E or U+FE0F (text or emoji presentation) after an
81
+ * Extended_Pictographic character, or between #, * or a digit and U+20E3 (a keycap),
82
+ * the same rule as text-sanctifier/context. After a letter it is a run: one selector
83
+ * per letter can carry a bit each.
84
+ *
85
+ * `line` and `column` are counted as in inspectTextDetailed. Every run and benign
86
+ * span is counted; only the first `maxRuns` of each are listed. With the caps set,
87
+ * memory stays flat however large the input is. One pass over the string, no copy.
88
+ * Set both caps for untrusted input: without them, results grow with the input.
89
+ *
90
+ * @param {string} text
91
+ * @param {HiddenScanOptions} [options]
92
+ * @returns {HiddenTextScan}
93
+ */
94
+ export function findHiddenRuns(text, options = {}) {
95
+ return scanHiddenSpans(text, options);
96
+ }