text-sanctifier 1.3.4 → 1.3.6

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX } from './categories.generated.js';
4
4
  export { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX };
5
+ import { ASCII_KEYBOARD_SAFE_REGEX, EMOJI_REGEX, normalizeTypographicJank } from './textPatterns.js';
6
+ export { ASCII_KEYBOARD_SAFE_REGEX, EMOJI_REGEX, normalizeTypographicJank };
5
7
 
6
8
 
7
9
  /**
@@ -18,6 +20,8 @@ export { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX };
18
20
  * @property {boolean} [finalTrim]
19
21
  * @property {number} [maxLength]
20
22
  * @property {'throw'|'truncate'|'noop'} [onMaxLength]
23
+ * @property {boolean} [normalizeSpaces] - Replace look-alike spaces with an ordinary space (runs first).
24
+ * @property {boolean} [purgeZeroWidthSpaces] - Remove U+200B, U+2060 and U+FEFF only, keeping joiners and selectors.
21
25
  */
22
26
 
23
27
 
@@ -39,9 +43,13 @@ export { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX };
39
43
  * @param {boolean} [defaultOptions.finalTrim]
40
44
  * @param {number} [defaultOptions.maxLength=Infinity] - Hard input length cap (UTF-16 code units).
41
45
  * @param {'throw' | 'truncate' | 'noop'} [defaultOptions.onMaxLength='throw'] - Behavior when input exceeds maxLength.
46
+ * @param {boolean} [defaultOptions.normalizeSpaces]
47
+ * @param {boolean} [defaultOptions.purgeZeroWidthSpaces]
42
48
  * @returns {(text: string) => string}
43
49
  */
44
- export function summonSanctifier(defaultOptions = {}) {
50
+ function createSanctifier(defaultOptions = {}) {
51
+ const normalizeSpaces = !!defaultOptions.normalizeSpaces;
52
+ const purgeZeroWidthSpaces = !!defaultOptions.purgeZeroWidthSpaces;
45
53
  const purgeInvisibleChars = !!defaultOptions.purgeInvisibleChars;
46
54
  const purgeEmojis = !!defaultOptions.purgeEmojis;
47
55
  const nukeControls = !!defaultOptions.nukeControls;
@@ -79,11 +87,16 @@ export function summonSanctifier(defaultOptions = {}) {
79
87
  collapseSpaces,
80
88
  finalTrim,
81
89
  maxLength,
82
- onMaxLength
90
+ onMaxLength,
91
+ normalizeSpaces,
92
+ purgeZeroWidthSpaces
83
93
  );
84
94
  }
85
95
 
86
- // --- Added Presets ---
96
+ // --- Presets ---
97
+ //
98
+ // Each preset is its own export, so a bundle that imports one preset leaves out the
99
+ // others. summonSanctifier.strict and the rest are the same functions.
87
100
 
88
101
  /**
89
102
  * Strict sanitizer:
@@ -91,21 +104,24 @@ export function summonSanctifier(defaultOptions = {}) {
91
104
  * - Collapse all newlines
92
105
  * - Collapse spaces
93
106
  * - Nuke control characters
107
+ * @param {string} text
108
+ * @returns {string}
94
109
  */
95
- /** @param {string} text @returns {string} */
96
- summonSanctifier.strict = text => sanctifyText(
97
- text,
98
- true, // purgeInvisibleChars
99
- true, // purgeEmojis
100
- true, // nukeControls
101
- false, // keyboardOnlyFilter
102
- true, // normalizeNewlines
103
- true, // trimSpacesAroundNewlines
104
- true, // collapseNewLines
105
- false, // preserveParagraphs
106
- true, // collapseSpaces
107
- true // finalTrim
108
- );
110
+ export function sanctifyStrict(text) {
111
+ return sanctifyText(
112
+ text,
113
+ true, // purgeInvisibleChars
114
+ true, // purgeEmojis
115
+ true, // nukeControls
116
+ false, // keyboardOnlyFilter
117
+ true, // normalizeNewlines
118
+ true, // trimSpacesAroundNewlines
119
+ true, // collapseNewLines
120
+ false, // preserveParagraphs
121
+ true, // collapseSpaces
122
+ true // finalTrim
123
+ );
124
+ }
109
125
 
110
126
 
111
127
  /**
@@ -113,21 +129,24 @@ summonSanctifier.strict = text => sanctifyText(
113
129
  * - Collapse spaces
114
130
  * - Preserve paragraphs
115
131
  * - Normalize newlines
132
+ * @param {string} text
133
+ * @returns {string}
116
134
  */
117
- /** @param {string} text @returns {string} */
118
- summonSanctifier.loose = text => sanctifyText(
119
- text,
120
- false, // purgeInvisibleChars
121
- false, // purgeEmojis
122
- false, // nukeControls
123
- false, // keyboardOnlyFilter
124
- true, // normalizeNewlines
125
- true, // trimSpacesAroundNewlines
126
- true, // collapseNewLines
127
- true, // preserveParagraphs
128
- true, // collapseSpaces
129
- true // finalTrim
130
- );
135
+ export function sanctifyLoose(text) {
136
+ return sanctifyText(
137
+ text,
138
+ false, // purgeInvisibleChars
139
+ false, // purgeEmojis
140
+ false, // nukeControls
141
+ false, // keyboardOnlyFilter
142
+ true, // normalizeNewlines
143
+ true, // trimSpacesAroundNewlines
144
+ true, // collapseNewLines
145
+ true, // preserveParagraphs
146
+ true, // collapseSpaces
147
+ true // finalTrim
148
+ );
149
+ }
131
150
 
132
151
 
133
152
  /**
@@ -135,21 +154,24 @@ summonSanctifier.loose = text => sanctifyText(
135
154
  * - Keeps emojis and printable ASCII
136
155
  * - Strips non-standard characters
137
156
  * - Normalizes typographic trash
157
+ * @param {string} text
158
+ * @returns {string}
138
159
  */
139
- /** @param {string} text @returns {string} */
140
- summonSanctifier.keyboardOnlyEmoji = text => sanctifyText(
141
- text,
142
- false, // purgeInvisibleChars
143
- false, // purgeEmojis
144
- false, // nukeControls
145
- true, // keyboardOnlyFilter
146
- true, // normalizeNewlines
147
- true, // trimSpacesAroundNewlines
148
- false, // collapseNewLines
149
- false, // preserveParagraphs
150
- false, // collapseSpaces
151
- true // finalTrim
152
- );
160
+ export function sanctifyKeyboardOnlyEmoji(text) {
161
+ return sanctifyText(
162
+ text,
163
+ false, // purgeInvisibleChars
164
+ false, // purgeEmojis
165
+ false, // nukeControls
166
+ true, // keyboardOnlyFilter
167
+ true, // normalizeNewlines
168
+ true, // trimSpacesAroundNewlines
169
+ false, // collapseNewLines
170
+ false, // preserveParagraphs
171
+ false, // collapseSpaces
172
+ true // finalTrim
173
+ );
174
+ }
153
175
 
154
176
 
155
177
  /**
@@ -157,21 +179,97 @@ summonSanctifier.keyboardOnlyEmoji = text => sanctifyText(
157
179
  * - Removes emojis
158
180
  * - Collapses all whitespace
159
181
  * - Restricts to printable ASCII only
182
+ * @param {string} text
183
+ * @returns {string}
160
184
  */
161
- /** @param {string} text @returns {string} */
162
- summonSanctifier.keyboardOnly = text => sanctifyText(
163
- text,
164
- true, // purgeInvisibleChars
165
- true, // purgeEmojis
166
- true, // nukeControls
167
- true, // keyboardOnlyFilter
168
- true, // normalizeNewlines
169
- true, // trimSpacesAroundNewlines
170
- true, // collapseNewLines
171
- false, // preserveParagraphs
172
- true, // collapseSpaces
173
- true // finalTrim
174
- );
185
+ export function sanctifyKeyboardOnly(text) {
186
+ return sanctifyText(
187
+ text,
188
+ true, // purgeInvisibleChars
189
+ true, // purgeEmojis
190
+ true, // nukeControls
191
+ true, // keyboardOnlyFilter
192
+ true, // normalizeNewlines
193
+ true, // trimSpacesAroundNewlines
194
+ true, // collapseNewLines
195
+ false, // preserveParagraphs
196
+ true, // collapseSpaces
197
+ true // finalTrim
198
+ );
199
+ }
200
+
201
+
202
+ /**
203
+ * Pasted text and model output:
204
+ * - Look-alike spaces (NBSP, narrow NBSP, typographic spaces) become ordinary spaces,
205
+ * so words never run together
206
+ * - Zero-width spaces, word joiners and BOMs are removed
207
+ * - Newlines are normalized to `\n`
208
+ * Keeps emoji joiners, variation selectors, bidi marks, indentation and line breaks.
209
+ * @param {string} text
210
+ * @returns {string}
211
+ */
212
+ export function sanctifyPasted(text) {
213
+ return sanctifyText(
214
+ text,
215
+ false, // purgeInvisibleChars
216
+ false, // purgeEmojis
217
+ false, // nukeControls
218
+ false, // keyboardOnlyFilter
219
+ true, // normalizeNewlines
220
+ false, // trimSpacesAroundNewlines
221
+ false, // collapseNewLines
222
+ false, // preserveParagraphs
223
+ false, // collapseSpaces
224
+ false, // finalTrim
225
+ Infinity, // maxLength
226
+ 'throw', // onMaxLength
227
+ true, // normalizeSpaces
228
+ true // purgeZeroWidthSpaces
229
+ );
230
+ }
231
+
232
+
233
+ /**
234
+ * Summons a customized sanctifier function with pre-bound booleans.
235
+ *
236
+ * Accepts full flag names and returns a text-cleaning function.
237
+ *
238
+ * @param {Object} [defaultOptions={}]
239
+ * @param {boolean} [defaultOptions.purgeInvisibleChars]
240
+ * @param {boolean} [defaultOptions.purgeEmojis]
241
+ * @param {boolean} [defaultOptions.nukeControls]
242
+ * @param {boolean} [defaultOptions.keyboardOnlyFilter]
243
+ * @param {boolean} [defaultOptions.normalizeNewlines]
244
+ * @param {boolean} [defaultOptions.trimSpacesAroundNewlines]
245
+ * @param {boolean} [defaultOptions.collapseNewLines]
246
+ * @param {boolean} [defaultOptions.preserveParagraphs]
247
+ * @param {boolean} [defaultOptions.collapseSpaces]
248
+ * @param {boolean} [defaultOptions.finalTrim]
249
+ * @param {number} [defaultOptions.maxLength=Infinity] - Hard input length cap (UTF-16 code units).
250
+ * @param {'throw' | 'truncate' | 'noop'} [defaultOptions.onMaxLength='throw'] - Behavior when input exceeds maxLength.
251
+ * @param {boolean} [defaultOptions.normalizeSpaces]
252
+ * @param {boolean} [defaultOptions.purgeZeroWidthSpaces]
253
+ * @returns {(text: string) => string}
254
+ *
255
+ * The presets are attached as properties (`summonSanctifier.strict`, `.loose`,
256
+ * `.keyboardOnlyEmoji`, `.keyboardOnly`, `.pasted`) and are also standalone exports.
257
+ * Built in a call marked pure, so a bundle that imports only a preset leaves it out.
258
+ * @type {typeof createSanctifier & {
259
+ * strict: typeof sanctifyStrict,
260
+ * loose: typeof sanctifyLoose,
261
+ * keyboardOnlyEmoji: typeof sanctifyKeyboardOnlyEmoji,
262
+ * keyboardOnly: typeof sanctifyKeyboardOnly,
263
+ * pasted: typeof sanctifyPasted,
264
+ * }}
265
+ */
266
+ export const summonSanctifier = /* @__PURE__ */ Object.assign(createSanctifier, {
267
+ strict: sanctifyStrict,
268
+ loose: sanctifyLoose,
269
+ keyboardOnlyEmoji: sanctifyKeyboardOnlyEmoji,
270
+ keyboardOnly: sanctifyKeyboardOnly,
271
+ pasted: sanctifyPasted,
272
+ });
175
273
 
176
274
 
177
275
  /**
@@ -198,6 +296,9 @@ summonSanctifier.keyboardOnly = text => sanctifyText(
198
296
  * - `'throw'`: throw a `RangeError`
199
297
  * - `'truncate'`: slice to `maxLength` before processing
200
298
  * - `'noop'`: return the original input unchanged (skips processing)
299
+ * @param {boolean} [normalizeSpaces=false] - Replace look-alike spaces with an ordinary space. Runs first,
300
+ * so `purgeInvisibleChars` no longer joins the words around them.
301
+ * @param {boolean} [purgeZeroWidthSpaces=false] - Remove zero-width spaces, word joiners and BOMs only.
201
302
  * @returns {string}
202
303
  * @throws {TypeError} If `text` is not a string.
203
304
  * @throws {RangeError} If input exceeds `maxLength` and `onMaxLength` is `'throw'`.
@@ -215,7 +316,9 @@ export function sanctifyText(
215
316
  collapseSpaces = false,
216
317
  finalTrim = false,
217
318
  maxLength = Infinity,
218
- onMaxLength = 'throw'
319
+ onMaxLength = 'throw',
320
+ normalizeSpaces = false,
321
+ purgeZeroWidthSpaces = false
219
322
  ) {
220
323
  if (typeof text !== 'string') {
221
324
  throw new TypeError('sanctifyText expects a string input.');
@@ -242,6 +345,12 @@ export function sanctifyText(
242
345
 
243
346
  let cleaned = text;
244
347
 
348
+ // Look-alike spaces become ordinary spaces, before anything can delete them
349
+ if (normalizeSpaces) cleaned = cleaned.replace(SPACE_LOOKALIKES_REGEX, ' ');
350
+
351
+ // Zero-width spaces, word joiners and BOMs (not joiners or selectors)
352
+ if (purgeZeroWidthSpaces) cleaned = cleaned.replace(ZERO_WIDTH_SPACES_REGEX, '');
353
+
245
354
  // Purge invisible Unicode trash (zero-width, non-breaking, bidi junk, etc.)
246
355
  if (purgeInvisibleChars) cleaned = purgeInvisibleTrash(cleaned);
247
356
 
@@ -274,6 +383,16 @@ export function sanctifyText(
274
383
  // --- Micro helpers ---
275
384
 
276
385
 
386
+ /**
387
+ * Look-alike spaces: the `nonbreaking_space` category except U+3000, the ordinary
388
+ * space of CJK text. A test checks this against the category table.
389
+ */
390
+ export const SPACE_LOOKALIKES_REGEX = /[\u00A0\u2000-\u200A\u202F\u205F]/g;
391
+
392
+ /** Zero-width space, word joiner and BOM: invisible and never needed in plain text. */
393
+ export const ZERO_WIDTH_SPACES_REGEX = /[\u200B\u2060\uFEFF]/g;
394
+
395
+
277
396
  /**
278
397
  * Removes the invisible Unicode characters in INVISIBLE_TRASH_REGEX: non-breaking and
279
398
  * typographic spaces, zero-width characters, bidi controls, invisible format characters,
@@ -317,13 +436,6 @@ function extractEmojiSequences(text) {
317
436
  return out;
318
437
  }
319
438
 
320
- /**
321
- * Matches any character that is NOT:
322
- * - Printable ASCII (U+0020–U+007E)
323
- * - Newline characters: \n (LF), \r (CR)
324
- * This allows for \n, \r, and \r\n line endings.
325
- */
326
- export const ASCII_KEYBOARD_SAFE_REGEX = /[^\x20-\x7E\r\n]+/gu;
327
439
 
328
440
  /**
329
441
  * Removes all non-keyboard characters, preserving:
@@ -345,64 +457,6 @@ function purgeNonKeyboardChars(text, purgeEmojis = false) {
345
457
  }
346
458
 
347
459
 
348
- // Smart apostrophes (single quote variations)
349
- const SMART_SINGLE_QUOTES_REGEX = /[\u2018\u2019\u201A\u201B\u2032\u2035]/g;
350
-
351
- // Smart double quotes (including primes and guillemets)
352
- const SMART_DOUBLE_QUOTES_REGEX = /[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g;
353
-
354
- // Dash variants: en dash, em dash, horizontal bar, figure dash, minus
355
- const UNICODE_DASHES_REGEX = /[\u2012\u2013\u2014\u2015\u2212]/g;
356
-
357
- // Ellipsis character
358
- const ELLIPSIS_REGEX = /\u2026/g;
359
-
360
- // Bullets and middle dot
361
- const BULLETS_REGEX = /[\u2022\u00B7]/g;
362
-
363
- // Full-width ASCII punctuation: U+FF01 - U+FF5E
364
- const FULLWIDTH_PUNCTUATION_REGEX = /[\uFF01-\uFF5E]/g;
365
-
366
- /**
367
- * Normalizes typographic Unicode punctuation into ASCII equivalents.
368
- * @param {string} text
369
- * @returns {string}
370
- */
371
- export function normalizeTypographicJank(text) {
372
- return text
373
- .replace(SMART_SINGLE_QUOTES_REGEX, "'")
374
- .replace(SMART_DOUBLE_QUOTES_REGEX, '"')
375
- .replace(UNICODE_DASHES_REGEX, '-')
376
- .replace(ELLIPSIS_REGEX, '...')
377
- .replace(BULLETS_REGEX, '*')
378
- .replace(FULLWIDTH_PUNCTUATION_REGEX, m =>
379
- String.fromCharCode(m.charCodeAt(0) - 0xFEE0)
380
- );
381
- }
382
-
383
-
384
- /** @type {RegExp} */
385
- export let EMOJI_REGEX;
386
-
387
- /**
388
- * Try Unicode property escape regex (preferred).
389
- * Fallback to basic emoji ranges if unsupported.
390
- */
391
- try {
392
- EMOJI_REGEX = new RegExp(
393
- '(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)',
394
- 'gu'
395
- );
396
- } catch {
397
- // Fallback: wide-range emoji component match (flags, tones, symbols)
398
- // * Covers:
399
- // * - Emoji base chars (1F300–1FAFF)
400
- // * - Dingbats (2700–27BF) like ❌, ✅, ☑️
401
- // * - Skin tones (1F3FB–1F3FF)
402
- // * - ZWJ (200D), variation selectors (FE0F)
403
- // * - Regional indicators (1F1E6–1F1FF)
404
- EMOJI_REGEX = /[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu;
405
- }
406
460
 
407
461
  /**
408
462
  * Removes all emoji characters using Unicode property escapes.
@@ -0,0 +1,78 @@
1
+ // src/textPatterns.js
2
+ //
3
+ // Patterns shared by the inspector and the sanitizer. They live apart from
4
+ // sanctifyText.js so that importing an inspector does not pull in the sanitizer
5
+ // engine and its presets (see the size probes in scripts/check-size.js).
6
+
7
+
8
+ /**
9
+ * Matches any character that is NOT:
10
+ * - Printable ASCII (U+0020–U+007E)
11
+ * - Newline characters: \n (LF), \r (CR)
12
+ * This allows for \n, \r, and \r\n line endings.
13
+ */
14
+ export const ASCII_KEYBOARD_SAFE_REGEX = /[^\x20-\x7E\r\n]+/gu;
15
+
16
+
17
+ // Smart apostrophes (single quote variations)
18
+ const SMART_SINGLE_QUOTES_REGEX = /[\u2018\u2019\u201A\u201B\u2032\u2035]/g;
19
+
20
+ // Smart double quotes (including primes and guillemets)
21
+ const SMART_DOUBLE_QUOTES_REGEX = /[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g;
22
+
23
+ // Dash variants: en dash, em dash, horizontal bar, figure dash, minus
24
+ const UNICODE_DASHES_REGEX = /[\u2012\u2013\u2014\u2015\u2212]/g;
25
+
26
+ // Ellipsis character
27
+ const ELLIPSIS_REGEX = /\u2026/g;
28
+
29
+ // Bullets and middle dot
30
+ const BULLETS_REGEX = /[\u2022\u00B7]/g;
31
+
32
+ // Full-width ASCII punctuation: U+FF01 - U+FF5E
33
+ const FULLWIDTH_PUNCTUATION_REGEX = /[\uFF01-\uFF5E]/g;
34
+
35
+ /**
36
+ * Normalizes typographic Unicode punctuation into ASCII equivalents.
37
+ * @param {string} text
38
+ * @returns {string}
39
+ */
40
+ export function normalizeTypographicJank(text) {
41
+ return text
42
+ .replace(SMART_SINGLE_QUOTES_REGEX, "'")
43
+ .replace(SMART_DOUBLE_QUOTES_REGEX, '"')
44
+ .replace(UNICODE_DASHES_REGEX, '-')
45
+ .replace(ELLIPSIS_REGEX, '...')
46
+ .replace(BULLETS_REGEX, '*')
47
+ .replace(FULLWIDTH_PUNCTUATION_REGEX, m =>
48
+ String.fromCharCode(m.charCodeAt(0) - 0xFEE0)
49
+ );
50
+ }
51
+
52
+
53
+ /**
54
+ * Try Unicode property escape regex (preferred).
55
+ * Fallback to basic emoji ranges if unsupported.
56
+ * Built in a function marked pure, so bundlers drop it when nothing uses it.
57
+ * @returns {RegExp}
58
+ */
59
+ function buildEmojiRegex() {
60
+ try {
61
+ return new RegExp(
62
+ '(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)',
63
+ 'gu'
64
+ );
65
+ } catch {
66
+ // Fallback: wide-range emoji component match (flags, tones, symbols)
67
+ // * Covers:
68
+ // * - Emoji base chars (1F300–1FAFF)
69
+ // * - Dingbats (2700–27BF) like ❌, ✅, ☑
70
+ // * - Skin tones (1F3FB–1F3FF)
71
+ // * - ZWJ (200D), variation selectors (FE0F)
72
+ // * - Regional indicators (1F1E6–1F1FF)
73
+ return /[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu;
74
+ }
75
+ }
76
+
77
+ /** @type {RegExp} */
78
+ export const EMOJI_REGEX = /* @__PURE__ */ buildEmojiRegex();
@@ -0,0 +1,5 @@
1
+ // src/version.generated.js
2
+ // Generated from package.json by scripts/gen-version.js (`npm run gen`); do not edit.
3
+
4
+ /** The installed version of text-sanctifier, as in its package.json. */
5
+ export const VERSION = '1.3.6';