text-sanctifier 1.3.5 → 1.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-sanctifier",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "type": "module",
5
5
  "description": "A brutal text normalizer and invisible trash scrubber for modern web projects.",
6
6
  "engines": {
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/types/hiddenText.d.ts",
32
32
  "default": "./src/hiddenText.js"
33
33
  },
34
+ "./context": {
35
+ "types": "./dist/types/context.d.ts",
36
+ "default": "./src/context.js"
37
+ },
34
38
  "./browser": {
35
39
  "types": "./dist/types/index.d.ts",
36
40
  "default": "./dist/text-sanctifier.min.js"
@@ -39,12 +43,12 @@
39
43
  "types": "./dist/types/index.d.ts",
40
44
  "sideEffects": false,
41
45
  "scripts": {
42
- "gen": "node scripts/gen-categories.js",
43
- "gen:check": "node scripts/gen-categories.js --check",
46
+ "gen": "node scripts/gen-categories.js && node scripts/gen-version.js",
47
+ "gen:check": "node scripts/gen-categories.js --check && node scripts/gen-version.js --check",
44
48
  "build": "node scripts/build.js",
45
49
  "build:types": "tsc -p tsconfig.types.json",
46
50
  "test:types": "tsc -p tests/types/tsconfig.json",
47
- "test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node tests/cli.test.js",
51
+ "test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node --expose-gc tests/hiddenText.test.js && node --expose-gc tests/context.test.js && node tests/cli.test.js",
48
52
  "test-min": "node tests/sanctifyText.test.min.js",
49
53
  "ci": "npm run gen:check && npm run test && npm run build && npm run size && npm run build:types && npm run test:types && npm run test-min",
50
54
  "size": "node scripts/check-size.js"
package/src/context.js ADDED
@@ -0,0 +1,177 @@
1
+ // src/context.js
2
+ //
3
+ // inspectTextInContext: inspectTextDetailed plus which findings belong to legitimate
4
+ // sequences (emoji joiners and selectors, ideographic variation sequences, Mongolian
5
+ // selectors, subdivision flags), counted exactly per category. The categories are
6
+ // unchanged; see "Proposal: context-aware findings" in DESIGN.md.
7
+ //
8
+ // Exported as `text-sanctifier/context`. It needs Unicode property escapes in regexes,
9
+ // so it lives apart from the core and the browser file and has its own size budget.
10
+
11
+ import { inspectTextDetailed } from './inspectText.js';
12
+
13
+ /** @typedef {import('./inspectText.js').TrashFinding} TrashFinding */
14
+ /** @typedef {import('./inspectText.js').TrashCounts} TrashCounts */
15
+ /** @typedef {import('./inspectText.js').InspectDetailedOptions} InspectDetailedOptions */
16
+
17
+ /**
18
+ * @typedef {TrashFinding & { benign: boolean }} ContextFinding
19
+ * A finding with `benign`: true when the character is part of a legitimate sequence.
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} ContextReport
24
+ * @property {ContextFinding[]} findings - As in inspectTextDetailed, each with `benign`.
25
+ * @property {TrashCounts} counts - As in inspectTextDetailed: every occurrence, benign or not.
26
+ * @property {number} total - Sum of `counts`.
27
+ * @property {boolean} truncated - True when `total` exceeds `findings.length`.
28
+ * @property {TrashCounts} benignCounts - Benign occurrences per category, listed or not.
29
+ * `counts[c] - benignCounts[c]` is the number of suspicious characters in category `c`.
30
+ * @property {number} benignTotal - Sum of `benignCounts`.
31
+ */
32
+
33
+ // Tested at an index of the text itself, so nothing is allocated per character.
34
+ const PICTOGRAPHIC_AT = /\p{Extended_Pictographic}/yu;
35
+ const IDEOGRAPHIC_AT = /\p{Ideographic}/yu;
36
+
37
+ // RGI subdivision flags: U+1F3F4, the tag letters, U+E007F
38
+ const SUBDIVISION_FLAGS = ['gbeng', 'gbsct', 'gbwls'];
39
+
40
+ /**
41
+ * @param {RegExp} re - sticky
42
+ * @param {string} text
43
+ * @param {number} index
44
+ */
45
+ function matchesAt(re, text, index) {
46
+ re.lastIndex = index;
47
+ return re.test(text);
48
+ }
49
+
50
+ /**
51
+ * Index where the code point that ends just before `index` starts, or -1.
52
+ * @param {string} text
53
+ * @param {number} index
54
+ */
55
+ function previousStart(text, index) {
56
+ if (index <= 0) return -1;
57
+ const low = text.charCodeAt(index - 1);
58
+ if (low >= 0xDC00 && low <= 0xDFFF && index >= 2) {
59
+ const high = text.charCodeAt(index - 2);
60
+ if (high >= 0xD800 && high <= 0xDBFF) return index - 2;
61
+ }
62
+ return index - 1;
63
+ }
64
+
65
+ /** @param {number} c */
66
+ const isMongolianLetter = (c) =>
67
+ (c >= 0x1820 && c <= 0x1878) || (c >= 0x1880 && c <= 0x1884) || (c >= 0x1887 && c <= 0x18A8) || c === 0x18AA;
68
+
69
+ /**
70
+ * A U+200D is benign between two Extended_Pictographic code points, allowing a
71
+ * skin-tone modifier or VS16 after the first.
72
+ * @param {string} text
73
+ * @param {number} i - index of the U+200D
74
+ */
75
+ function isEmojiJoiner(text, i) {
76
+ let p = previousStart(text, i);
77
+ if (p < 0) return false;
78
+ const before = /** @type {number} */ (text.codePointAt(p));
79
+ if (before === 0xFE0F || (before >= 0x1F3FB && before <= 0x1F3FF)) p = previousStart(text, p);
80
+ return p >= 0 && matchesAt(PICTOGRAPHIC_AT, text, p) && matchesAt(PICTOGRAPHIC_AT, text, i + 1);
81
+ }
82
+
83
+ /**
84
+ * Length in UTF-16 units of the subdivision-flag tags starting at `i` (just after a
85
+ * U+1F3F4), or 0 when they are not exactly one of the RGI flags, as findHiddenRuns decides.
86
+ * @param {string} text
87
+ * @param {number} i
88
+ */
89
+ function flagTagsAt(text, i) {
90
+ // five tag letters and CANCEL TAG, each a surrogate pair
91
+ if (text.charCodeAt(i + 10) !== 0xDB40 || text.charCodeAt(i + 11) !== 0xDC7F) return 0;
92
+ // more tags after CANCEL TAG make it one longer tag run, which is not a flag
93
+ const after = text.charCodeAt(i + 13);
94
+ if (text.charCodeAt(i + 12) === 0xDB40 && after >= 0xDC00 && after <= 0xDC7F) return 0;
95
+ let letters = '';
96
+ for (let k = 0; k < 10; k += 2) {
97
+ const low = text.charCodeAt(i + k + 1);
98
+ if (text.charCodeAt(i + k) !== 0xDB40 || low < 0xDC61 || low > 0xDC7A) return 0; // tag a-z
99
+ letters += String.fromCharCode(low - 0xDC00);
100
+ }
101
+ return SUBDIVISION_FLAGS.includes(letters) ? 12 : 0;
102
+ }
103
+
104
+ /**
105
+ * Locates invisible and control characters as inspectTextDetailed does, and marks
106
+ * the ones that belong to legitimate sequences as benign:
107
+ * - U+200D between two emoji (Extended_Pictographic), allowing a skin-tone modifier or
108
+ * VS16 after the first: an emoji ZWJ sequence
109
+ * - U+FE0E or U+FE0F directly after an emoji, or between 0-9, # or * and U+20E3 (keycap)
110
+ * - one of U+E0100–U+E01EF directly after an ideograph: an ideographic variation sequence
111
+ * - U+180B–U+180D or U+180F directly after a Mongolian letter
112
+ * - the tags of the England, Scotland and Wales flags after U+1F3F4
113
+ * Nothing else is benign: not U+200B, U+2060, U+FEFF, bidi controls or other tags.
114
+ *
115
+ * `counts`, `total` and `truncated` are exactly those of inspectTextDetailed;
116
+ * `benignCounts` counts every benign character, listed or not. Two passes over the
117
+ * string, no copy; memory is flat with `maxFindings` set.
118
+ *
119
+ * @param {string} text
120
+ * @param {InspectDetailedOptions} [options]
121
+ * @returns {ContextReport}
122
+ */
123
+ export function inspectTextInContext(text, options = {}) {
124
+ if (typeof text !== 'string') {
125
+ throw new TypeError('inspectTextInContext expects a string input.');
126
+ }
127
+ const report = inspectTextDetailed(text, options);
128
+ const findings = /** @type {ContextFinding[]} */ (report.findings);
129
+ for (const f of findings) f.benign = false;
130
+
131
+ /** @type {TrashCounts} */
132
+ const benignCounts = /** @type {TrashCounts} */ ({});
133
+ for (const key of Object.keys(report.counts)) benignCounts[/** @type {keyof TrashCounts} */ (key)] = 0;
134
+ let benignTotal = 0;
135
+ let next = 0; // first listed finding not yet passed
136
+
137
+ /**
138
+ * @param {number} offset
139
+ * @param {keyof TrashCounts} category
140
+ */
141
+ const markBenign = (offset, category) => {
142
+ benignCounts[category]++;
143
+ benignTotal++;
144
+ while (next < findings.length && findings[next].offset < offset) next++;
145
+ if (next < findings.length && findings[next].offset === offset) findings[next].benign = true;
146
+ };
147
+
148
+ for (let i = 0, n = text.length; i < n; i++) {
149
+ const c = text.charCodeAt(i);
150
+ if (c < 0x180B) continue; // the common case: nothing below can be benign
151
+
152
+ if (c === 0x200D) {
153
+ if (isEmojiJoiner(text, i)) markBenign(i, 'zero_width');
154
+ } else if (c === 0xFE0E || c === 0xFE0F) {
155
+ const p = previousStart(text, i);
156
+ const before = p < 0 ? -1 : text.charCodeAt(p);
157
+ const keycap = (before === 0x23 || before === 0x2A || (before >= 0x30 && before <= 0x39)) &&
158
+ text.charCodeAt(i + 1) === 0x20E3;
159
+ if (keycap || (p >= 0 && matchesAt(PICTOGRAPHIC_AT, text, p))) markBenign(i, 'variation_selector');
160
+ } else if ((c >= 0x180B && c <= 0x180D) || c === 0x180F) {
161
+ if (isMongolianLetter(text.charCodeAt(i - 1))) markBenign(i, 'variation_selector');
162
+ } else if (c === 0xD83C && text.charCodeAt(i + 1) === 0xDFF4) { // U+1F3F4
163
+ const length = flagTagsAt(text, i + 2);
164
+ for (let k = 0; k < length; k += 2) markBenign(i + 2 + k, 'tag');
165
+ i += 1 + length;
166
+ } else if (c === 0xDB40) { // plane 14: U+E0000-U+E03FF
167
+ const low = text.charCodeAt(i + 1);
168
+ if (low >= 0xDD00 && low <= 0xDDEF) { // U+E0100-U+E01EF
169
+ const p = previousStart(text, i);
170
+ if (p >= 0 && matchesAt(IDEOGRAPHIC_AT, text, p)) markBenign(i, 'variation_selector');
171
+ }
172
+ if (low >= 0xDC00 && low <= 0xDFFF) i++;
173
+ }
174
+ }
175
+
176
+ return { findings, counts: report.counts, total: report.total, truncated: report.truncated, benignCounts, benignTotal };
177
+ }
package/src/hiddenText.js CHANGED
@@ -1,14 +1,17 @@
1
1
  // src/hiddenText.js
2
2
  //
3
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
4
+ // tag characters, variation selectors, invisible operators and the reserved code points
5
+ // of the tag plane. Tag runs are decoded
5
6
  // to the ASCII they spell. Used by `text-sanctifier check --ai-config`, and exported
6
7
  // as `text-sanctifier/hidden`.
7
8
  //
8
9
  // Plain JavaScript with no Node APIs, so it also runs in a browser. src/index.js does
9
10
  // not import this file, so it stays out of the browser bundle and its size budget.
10
11
 
11
- /** @typedef {'tag' | 'variation_selector' | 'invisible_operator'} HiddenRunKind */
12
+ export { VERSION } from './version.generated.js';
13
+
14
+ /** @typedef {'tag' | 'variation_selector' | 'invisible_operator' | 'reserved_tag_plane'} HiddenRunKind */
12
15
 
13
16
  /**
14
17
  * @typedef {Object} HiddenRun
@@ -16,14 +19,41 @@
16
19
  * @property {number} offset - UTF-16 index of the first character
17
20
  * @property {number} end - UTF-16 index just past the last character
18
21
  * @property {number} count - number of code points in the run
19
- * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText)
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
20
35
  */
21
36
 
22
37
  /**
23
38
  * @typedef {Object} HiddenTextScan
24
- * @property {HiddenRun[]} runs - runs that may hide text, in order
39
+ * @property {HiddenRun[]} runs - runs that may hide text, in order, at most `maxRuns` long
25
40
  * @property {Array<[number, number]>} benign - [offset, end) spans of legitimate
26
- * sequences (subdivision flags, a single emoji or text presentation selector)
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.
27
57
  */
28
58
 
29
59
  const TAG_FIRST = 0xE0000;
@@ -43,6 +73,8 @@ function kindOf(cp) {
43
73
  if ((cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF) ||
44
74
  (cp >= 0x180B && cp <= 0x180D) || cp === 0x180F) return 'variation_selector';
45
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';
46
78
  return null;
47
79
  }
48
80
 
@@ -57,64 +89,145 @@ const hex = (cp) => 'U+' + cp.toString(16).toUpperCase().padStart(4, '0');
57
89
  * @returns {string}
58
90
  */
59
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) {
60
105
  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)}>`;
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;
64
113
  }
65
- return out;
114
+ return { decoded: out, truncated: false };
66
115
  }
67
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
+
68
121
  /**
69
- * Finds runs of tag characters, variation selectors and invisible operators.
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).
70
145
  * Consecutive characters of the same kind form one run. Two kinds of sequence are
71
146
  * legitimate and listed under `benign` instead of `runs`:
72
147
  * - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
73
148
  * tags for "gbeng", "gbsct" or "gbwls" and U+E007F
74
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
+ *
75
156
  * @param {string} text
157
+ * @param {HiddenScanOptions} [options]
76
158
  * @returns {HiddenTextScan}
77
159
  */
78
- export function findHiddenRuns(text) {
160
+ export function findHiddenRuns(text, options = {}) {
161
+ const maxRuns = cap(options && options.maxRuns);
162
+ const maxDecodedLength = cap(options && options.maxDecodedLength);
163
+
79
164
  /** @type {HiddenRun[]} */
80
165
  const runs = [];
81
166
  /** @type {Array<[number, number]>} */
82
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;
83
177
 
84
178
  let i = 0;
85
179
  while (i < text.length) {
86
180
  const cp = /** @type {number} */ (text.codePointAt(i));
87
181
  const kind = kindOf(cp);
88
- if (kind === null) { i += cp > 0xFFFF ? 2 : 1; continue; }
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
+ }
89
190
 
90
191
  const offset = i;
91
192
  let count = 0;
193
+ let supplementary = 0;
92
194
  let last = cp;
93
195
  while (i < text.length) {
94
196
  const c = /** @type {number} */ (text.codePointAt(i));
95
197
  if (kindOf(c) !== kind) break;
96
198
  last = c;
97
199
  count++;
200
+ if (c >= 0xE0100 && c <= 0xE01EF) supplementary++;
98
201
  i += c > 0xFFFF ? 2 : 1;
99
202
  }
100
203
  const end = i;
101
204
 
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
- }
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]);
110
214
  continue;
111
215
  }
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;
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);
116
230
  }
117
- runs.push({ kind, offset, end, count });
118
231
  }
119
- return { runs, benign };
232
+ return { runs, benign, counts, codePointCounts, supplementarySelectorCount, total, benignTotal, truncated: total > runs.length || benignTotal > benign.length };
120
233
  }
package/src/index.d.ts CHANGED
@@ -3,8 +3,14 @@ import { inspectTextDetailed } from './inspectText.js';
3
3
  import { getNewlineStats } from './inspectText.js';
4
4
  import { getRecommendedSanctifierOptions } from './inspectText.js';
5
5
  import { summonSanctifier } from './sanctifyText.js';
6
+ import { sanctifyStrict } from './sanctifyText.js';
7
+ import { sanctifyLoose } from './sanctifyText.js';
8
+ import { sanctifyKeyboardOnlyEmoji } from './sanctifyText.js';
9
+ import { sanctifyKeyboardOnly } from './sanctifyText.js';
10
+ import { sanctifyPasted } from './sanctifyText.js';
6
11
  import { revealInvisible } from './reveal.js';
7
- export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier, revealInvisible };
12
+ import { VERSION } from './version.generated.js';
13
+ export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier, sanctifyStrict, sanctifyLoose, sanctifyKeyboardOnlyEmoji, sanctifyKeyboardOnly, sanctifyPasted, revealInvisible, VERSION };
8
14
  export type UnicodeTrashReport = import('./inspectText.js').UnicodeTrashReport;
9
15
  export type TrashCategory = import('./inspectText.js').TrashCategory;
10
16
  export type TrashFinding = import('./inspectText.js').TrashFinding;
package/src/index.js CHANGED
@@ -4,12 +4,15 @@
4
4
  import { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions } from './inspectText.js';
5
5
  export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions };
6
6
 
7
- import { summonSanctifier } from './sanctifyText.js';
8
- export { summonSanctifier };
7
+ import { summonSanctifier, sanctifyStrict, sanctifyLoose, sanctifyKeyboardOnlyEmoji, sanctifyKeyboardOnly, sanctifyPasted } from './sanctifyText.js';
8
+ export { summonSanctifier, sanctifyStrict, sanctifyLoose, sanctifyKeyboardOnlyEmoji, sanctifyKeyboardOnly, sanctifyPasted };
9
9
 
10
10
  import { revealInvisible } from './reveal.js';
11
11
  export { revealInvisible };
12
12
 
13
+ import { VERSION } from './version.generated.js';
14
+ export { VERSION };
15
+
13
16
  // Type-only re-exports so TypeScript consumers can name these from the package root.
14
17
  /** @typedef {import('./inspectText.js').UnicodeTrashReport} UnicodeTrashReport */
15
18
  /** @typedef {import('./inspectText.js').TrashCategory} TrashCategory */
@@ -1,13 +1,7 @@
1
1
  // ./src/inspectText.js
2
2
 
3
- import {
4
- CONTROL_CHARS_REGEX,
5
- INVISIBLE_TRASH_REGEX,
6
- EMOJI_REGEX,
7
- ASCII_KEYBOARD_SAFE_REGEX,
8
- normalizeTypographicJank,
9
- } from './sanctifyText.js';
10
- import { classifyCodeUnit, classifyAstral } from './categories.generated.js';
3
+ import { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX, classifyCodeUnit, classifyAstral } from './categories.generated.js';
4
+ import { EMOJI_REGEX, ASCII_KEYBOARD_SAFE_REGEX, normalizeTypographicJank } from './textPatterns.js';
11
5
 
12
6
  /** @typedef {import('./sanctifyText.js').SanctifyOptions} SanctifyOptions */
13
7