text-sanctifier 1.3.1 → 1.3.2

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.
@@ -7,6 +7,7 @@ import {
7
7
  ASCII_KEYBOARD_SAFE_REGEX,
8
8
  normalizeTypographicJank,
9
9
  } from './sanctifyText.js';
10
+ import { classifyCodeUnit, classifyAstral } from './categories.generated.js';
10
11
 
11
12
  /** @typedef {import('./sanctifyText.js').SanctifyOptions} SanctifyOptions */
12
13
 
@@ -172,69 +173,21 @@ export function inspectText(text) {
172
173
 
173
174
  const DEFAULT_MAX_FINDINGS = 100;
174
175
 
175
- /**
176
- * Category of one BMP code point (one UTF-16 code unit), or null when it is not trash.
177
- * Tab, LF and CR are whitespace, not trash. DEL (U+007F) is counted with the C0 controls.
178
- * Surrogates return null; the caller pairs them and uses classifyAstral.
179
- * @param {number} c
180
- * @returns {TrashCategory|null}
181
- */
182
- function classifyCodeUnit(c) {
183
- if (c < 0x80) {
184
- if (c === 0) return 'nul';
185
- if (c < 0x20) return c === 0x09 || c === 0x0A || c === 0x0D ? null : 'c0_control';
186
- return c === 0x7F ? 'c0_control' : null;
187
- }
188
- if (c <= 0x9F) return 'c1_control';
189
- if (c === 0xA0) return 'nonbreaking_space';
190
- if (c === 0xAD || c === 0x034F || c === 0x180E) return 'invisible_format';
191
- if (c === 0x061C) return 'bidi_control';
192
- if (c === 0x115F || c === 0x1160) return 'hangul_filler';
193
- if (c < 0x2000) return null;
194
- if (c <= 0x200A) return 'nonbreaking_space';
195
- if (c <= 0x200D) return 'zero_width';
196
- if (c <= 0x200F) return 'bidi_control';
197
- if (c >= 0x202A && c <= 0x202E) return 'bidi_control';
198
- if (c === 0x202F || c === 0x205F) return 'nonbreaking_space';
199
- if (c === 0x2060) return 'zero_width';
200
- if (c >= 0x2061 && c <= 0x2064) return 'invisible_format';
201
- if (c >= 0x2066 && c <= 0x2069) return 'bidi_control';
202
- if (c === 0x3000) return 'nonbreaking_space';
203
- if (c === 0x3164 || c === 0xFFA0) return 'hangul_filler';
204
- if (c >= 0xFE00 && c <= 0xFE0F) return 'variation_selector';
205
- if (c === 0xFEFF) return 'bom';
206
- return null;
207
- }
208
-
209
- /**
210
- * Category of a code point above U+FFFF, or null when it is not trash.
211
- * @param {number} cp
212
- * @returns {TrashCategory|null}
213
- */
214
- function classifyAstral(cp) {
215
- if (cp >= 0xE0000 && cp <= 0xE007F) return 'tag';
216
- if (cp >= 0xE0100 && cp <= 0xE01EF) return 'variation_selector';
217
- return null;
218
- }
219
-
220
- /** High surrogate of U+E0000–U+E03FF, the only astral block with trash (tags, VS17–VS256). */
221
- const TRASH_HIGH_SURROGATE = 0xDB40;
222
-
223
176
  /**
224
177
  * Locates invisible and control characters, one finding per character.
225
178
  *
226
- * Categories:
179
+ * Categories (generated from scripts/categories.js by `npm run gen`):
227
180
  * - `bidi_control`: U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069
228
181
  * - `zero_width`: U+200B–U+200D, U+2060
229
- * - `nonbreaking_space`: non-breaking and typographic spaces U+00A0, U+2000–U+200A, U+202F, U+205F, U+3000
230
- * - `bom`: U+FEFF (`atStart` says whether it sits at offset 0)
182
+ * - `nonbreaking_space`: U+00A0, U+2000–U+200A, U+202F, U+205F, U+3000 (non-breaking and typographic spaces)
183
+ * - `bom`: U+FEFF (`atStart` is true at offset 0, where a BOM is legitimate)
231
184
  * - `nul`: U+0000
232
- * - `c0_control`: U+0001–U+001F except tab, LF and CR; and U+007F
185
+ * - `c0_control`: U+0001–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F (tab, LF and CR are whitespace, not trash)
233
186
  * - `c1_control`: U+0080–U+009F
234
- * - `invisible_format`: soft hyphen U+00AD, U+034F, U+180E, invisible operators U+2061–U+2064
187
+ * - `invisible_format`: U+00AD (soft hyphen), U+034F (combining grapheme joiner), U+17B4, U+17B5 (Khmer inherent vowels), U+180E (Mongolian vowel separator), U+2061–U+2064 (invisible operators), U+2065 (reserved), U+206A–U+206F (deprecated format characters), U+FFF0–U+FFF8 (reserved), U+1BCA0–U+1BCA3 (shorthand format controls), U+1D173–U+1D17A (musical formatting), U+E0080–U+E00FF (reserved), U+E01F0–U+E0FFF (reserved) (every other Default_Ignorable_Code_Point, which renderers draw as nothing)
235
188
  * - `hangul_filler`: U+115F, U+1160, U+3164, U+FFA0 (letters that render blank)
236
- * - `variation_selector`: U+FE00–U+FE0F, U+E0100–U+E01EF (including VS16 in emoji)
237
- * - `tag`: U+E0000–U+E007F (tag characters, used for "ASCII smuggling")
189
+ * - `variation_selector`: U+180B–U+180D (Mongolian FVS1-FVS3), U+180F (Mongolian FVS4), U+FE00–U+FE0F (VS1-VS16, including VS16 in emoji), U+E0100–U+E01EF (VS17-VS256)
190
+ * - `tag`: U+E0000–U+E007F (tag characters, used for "ASCII smuggling" of hidden instructions)
238
191
  *
239
192
  * `codePoint` is the full code point; `offset` and `column` count UTF-16 units.
240
193
  * Every occurrence is counted; only the first `maxFindings` are listed.
@@ -298,7 +251,7 @@ export function inspectTextDetailed(text, options = {}) {
298
251
  const offset = i;
299
252
  let codePoint = c;
300
253
  let category;
301
- if (c === TRASH_HIGH_SURROGATE) {
254
+ if (c >= 0xD800 && c <= 0xDBFF) {
302
255
  const low = text.charCodeAt(i + 1);
303
256
  if (!(low >= 0xDC00 && low <= 0xDFFF)) continue; // lone surrogate
304
257
  codePoint = 0x10000 + ((c - 0xD800) << 10) + (low - 0xDC00);
@@ -1,5 +1,8 @@
1
1
  // src/sanctifyText.js
2
2
 
3
+ import { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX } from './categories.generated.js';
4
+ export { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX };
5
+
3
6
 
4
7
  /**
5
8
  * @typedef {Object} SanctifyOptions
@@ -272,21 +275,14 @@ export function sanctifyText(
272
275
 
273
276
 
274
277
  /**
275
- * Purges invisible Unicode "trash" characters and removes them.
276
- *
277
- * Targets:
278
- * - Non-breaking spaces ( )
279
- * - Zero-width spaces and miscellaneous Unicode spaces ( –‍,  ,  , ⁠,  , )
280
- * - Bidi controls: marks, embeddings/overrides and isolates (؜, ‎, ‏, ‪–‮, ⁦–⁩)
281
- * - Invisible format characters: soft hyphen, CGJ, Mongolian vowel separator, invisible operators (­, ͏, ᠎, ⁡–⁤)
282
- * - Hangul fillers (ᅟ, ᅠ, ㅤ, ᅠ)
283
- * - Variation selectors (︀–️, \u{E0100}–\u{E01EF}), including VS16 in emoji
284
- * - Tag characters (\u{E0000}–\u{E007F}), used for "ASCII smuggling"
278
+ * Removes the invisible Unicode characters in INVISIBLE_TRASH_REGEX: non-breaking and
279
+ * typographic spaces, zero-width characters, bidi controls, invisible format characters,
280
+ * Hangul fillers, variation selectors (including VS16 in emoji), the BOM and tag characters.
281
+ * The code points come from the table in scripts/categories.js.
285
282
  *
286
283
  * @param {string} text
287
284
  * @returns {string}
288
285
  */
289
- export const INVISIBLE_TRASH_REGEX = /[ ­͏؜ᅟᅠ᠎ -‏‪-  ⁠-⁤⁦-⁩ ㅤ︀-️ᅠ\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}]+/gu;
290
286
  function purgeInvisibleTrash(text) {
291
287
  return text.replace(INVISIBLE_TRASH_REGEX, '');
292
288
  }
@@ -497,15 +493,14 @@ function collapseExtraSpaces(text) {
497
493
  * Nukes hidden control characters that are invisible and often dangerous.
498
494
  * (Excludes necessary whitespace like \n and \t.)
499
495
  *
500
- * Control characters nuked:
501
- * - ASCII control range (0x00-0x1F, 0x7F)
502
- * - Unicode control range (0x80-0x9F)
503
- * - Bidi controls (U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069)
496
+ * Control characters nuked (the code points come from the table in scripts/categories.js):
497
+ * - NUL and the C0 controls except tab, LF and CR; DEL (U+007F)
498
+ * - C1 controls (U+0080-U+009F)
499
+ * - Bidi controls
504
500
  *
505
501
  * @param {string} text
506
502
  * @returns {string}
507
503
  */
508
- export const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u0080-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g;
509
504
  function purgeControlCharacters(text) {
510
505
  return text.replace(CONTROL_CHARS_REGEX, '');
511
506
  }