text-sanctifier 1.1.1 → 1.3.0
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/README.md +65 -4
- package/bin/text-sanctifier.js +519 -0
- package/dist/text-sanctifier.min.js +13 -11
- package/dist/types/inspectText.d.ts +12 -2
- package/dist/types/sanctifyText.d.ts +8 -4
- package/package.json +6 -2
- package/src/inspectText.js +386 -341
- package/src/sanctifyText.js +511 -507
package/src/sanctifyText.js
CHANGED
|
@@ -1,507 +1,511 @@
|
|
|
1
|
-
// src/sanctifyText.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {Object} SanctifyOptions
|
|
6
|
-
* @property {boolean} [purgeInvisibleChars]
|
|
7
|
-
* @property {boolean} [purgeEmojis]
|
|
8
|
-
* @property {boolean} [nukeControls]
|
|
9
|
-
* @property {boolean} [keyboardOnlyFilter]
|
|
10
|
-
* @property {boolean} [normalizeNewlines]
|
|
11
|
-
* @property {boolean} [trimSpacesAroundNewlines]
|
|
12
|
-
* @property {boolean} [collapseNewLines]
|
|
13
|
-
* @property {boolean} [preserveParagraphs]
|
|
14
|
-
* @property {boolean} [collapseSpaces]
|
|
15
|
-
* @property {boolean} [finalTrim]
|
|
16
|
-
* @property {number} [maxLength]
|
|
17
|
-
* @property {'throw'|'truncate'|'noop'} [onMaxLength]
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Summons a customized sanctifier function with pre-bound booleans.
|
|
23
|
-
*
|
|
24
|
-
* Accepts full flag names and returns a text-cleaning function.
|
|
25
|
-
*
|
|
26
|
-
* @param {Object} [defaultOptions={}]
|
|
27
|
-
* @param {boolean} [defaultOptions.purgeInvisibleChars]
|
|
28
|
-
* @param {boolean} [defaultOptions.purgeEmojis]
|
|
29
|
-
* @param {boolean} [defaultOptions.nukeControls]
|
|
30
|
-
* @param {boolean} [defaultOptions.keyboardOnlyFilter]
|
|
31
|
-
* @param {boolean} [defaultOptions.normalizeNewlines]
|
|
32
|
-
* @param {boolean} [defaultOptions.trimSpacesAroundNewlines]
|
|
33
|
-
* @param {boolean} [defaultOptions.collapseNewLines]
|
|
34
|
-
* @param {boolean} [defaultOptions.preserveParagraphs]
|
|
35
|
-
* @param {boolean} [defaultOptions.collapseSpaces]
|
|
36
|
-
* @param {boolean} [defaultOptions.finalTrim]
|
|
37
|
-
* @param {number} [defaultOptions.maxLength=Infinity] - Hard input length cap (UTF-16 code units).
|
|
38
|
-
* @param {'throw' | 'truncate' | 'noop'} [defaultOptions.onMaxLength='throw'] - Behavior when input exceeds maxLength.
|
|
39
|
-
* @returns {(text: string) => string}
|
|
40
|
-
*/
|
|
41
|
-
export function summonSanctifier(defaultOptions = {}) {
|
|
42
|
-
const purgeInvisibleChars = !!defaultOptions.purgeInvisibleChars;
|
|
43
|
-
const purgeEmojis = !!defaultOptions.purgeEmojis;
|
|
44
|
-
const nukeControls = !!defaultOptions.nukeControls;
|
|
45
|
-
const keyboardOnlyFilter = !!defaultOptions.keyboardOnlyFilter;
|
|
46
|
-
const normalizeNewlines = !!defaultOptions.normalizeNewlines;
|
|
47
|
-
const trimSpacesAroundNewlines = !!defaultOptions.trimSpacesAroundNewlines;
|
|
48
|
-
const collapseNewLines = !!defaultOptions.collapseNewLines;
|
|
49
|
-
const preserveParagraphs = !!defaultOptions.preserveParagraphs;
|
|
50
|
-
const collapseSpaces = !!defaultOptions.collapseSpaces;
|
|
51
|
-
const finalTrim = !!defaultOptions.finalTrim;
|
|
52
|
-
|
|
53
|
-
const maxLength =
|
|
54
|
-
Number.isFinite(defaultOptions.maxLength) && defaultOptions.maxLength >= 0
|
|
55
|
-
? defaultOptions.maxLength
|
|
56
|
-
: Infinity;
|
|
57
|
-
|
|
58
|
-
const onMaxLength =
|
|
59
|
-
defaultOptions.onMaxLength === 'truncate' ||
|
|
60
|
-
defaultOptions.onMaxLength === 'noop' ||
|
|
61
|
-
defaultOptions.onMaxLength === 'throw'
|
|
62
|
-
? defaultOptions.onMaxLength
|
|
63
|
-
: 'throw';
|
|
64
|
-
|
|
65
|
-
return (text) =>
|
|
66
|
-
sanctifyText(
|
|
67
|
-
text,
|
|
68
|
-
purgeInvisibleChars,
|
|
69
|
-
purgeEmojis,
|
|
70
|
-
nukeControls,
|
|
71
|
-
keyboardOnlyFilter,
|
|
72
|
-
normalizeNewlines,
|
|
73
|
-
trimSpacesAroundNewlines,
|
|
74
|
-
collapseNewLines,
|
|
75
|
-
preserveParagraphs,
|
|
76
|
-
collapseSpaces,
|
|
77
|
-
finalTrim,
|
|
78
|
-
maxLength,
|
|
79
|
-
onMaxLength
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// --- Added Presets ---
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Strict sanitizer:
|
|
87
|
-
* - Purge emojis
|
|
88
|
-
* - Collapse all newlines
|
|
89
|
-
* - Collapse spaces
|
|
90
|
-
* - Nuke control characters
|
|
91
|
-
*/
|
|
92
|
-
/** @param {string} text @returns {string} */
|
|
93
|
-
summonSanctifier.strict = text => sanctifyText(
|
|
94
|
-
text,
|
|
95
|
-
true, // purgeInvisibleChars
|
|
96
|
-
true, // purgeEmojis
|
|
97
|
-
true, // nukeControls
|
|
98
|
-
false, // keyboardOnlyFilter
|
|
99
|
-
true, // normalizeNewlines
|
|
100
|
-
true, // trimSpacesAroundNewlines
|
|
101
|
-
true, // collapseNewLines
|
|
102
|
-
false, // preserveParagraphs
|
|
103
|
-
true, // collapseSpaces
|
|
104
|
-
true // finalTrim
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Loose sanitizer:
|
|
110
|
-
* - Collapse spaces
|
|
111
|
-
* - Preserve paragraphs
|
|
112
|
-
* - Normalize newlines
|
|
113
|
-
*/
|
|
114
|
-
/** @param {string} text @returns {string} */
|
|
115
|
-
summonSanctifier.loose = text => sanctifyText(
|
|
116
|
-
text,
|
|
117
|
-
false, // purgeInvisibleChars
|
|
118
|
-
false, // purgeEmojis
|
|
119
|
-
false, // nukeControls
|
|
120
|
-
false, // keyboardOnlyFilter
|
|
121
|
-
true, // normalizeNewlines
|
|
122
|
-
true, // trimSpacesAroundNewlines
|
|
123
|
-
true, // collapseNewLines
|
|
124
|
-
true, // preserveParagraphs
|
|
125
|
-
true, // collapseSpaces
|
|
126
|
-
true // finalTrim
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Keyboard-only (with emojis):
|
|
132
|
-
* - Keeps emojis and printable ASCII
|
|
133
|
-
* - Strips non-standard characters
|
|
134
|
-
* - Normalizes typographic trash
|
|
135
|
-
*/
|
|
136
|
-
/** @param {string} text @returns {string} */
|
|
137
|
-
summonSanctifier.keyboardOnlyEmoji = text => sanctifyText(
|
|
138
|
-
text,
|
|
139
|
-
false, // purgeInvisibleChars
|
|
140
|
-
false, // purgeEmojis
|
|
141
|
-
false, // nukeControls
|
|
142
|
-
true, // keyboardOnlyFilter
|
|
143
|
-
true, // normalizeNewlines
|
|
144
|
-
true, // trimSpacesAroundNewlines
|
|
145
|
-
false, // collapseNewLines
|
|
146
|
-
false, // preserveParagraphs
|
|
147
|
-
false, // collapseSpaces
|
|
148
|
-
true // finalTrim
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Keyboard-only (strict):
|
|
154
|
-
* - Removes emojis
|
|
155
|
-
* - Collapses all whitespace
|
|
156
|
-
* - Restricts to printable ASCII only
|
|
157
|
-
*/
|
|
158
|
-
/** @param {string} text @returns {string} */
|
|
159
|
-
summonSanctifier.keyboardOnly = text => sanctifyText(
|
|
160
|
-
text,
|
|
161
|
-
true, // purgeInvisibleChars
|
|
162
|
-
true, // purgeEmojis
|
|
163
|
-
true, // nukeControls
|
|
164
|
-
true, // keyboardOnlyFilter
|
|
165
|
-
true, // normalizeNewlines
|
|
166
|
-
true, // trimSpacesAroundNewlines
|
|
167
|
-
true, // collapseNewLines
|
|
168
|
-
false, // preserveParagraphs
|
|
169
|
-
true, // collapseSpaces
|
|
170
|
-
true // finalTrim
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Text Sanctifier
|
|
176
|
-
*
|
|
177
|
-
* Brutal text normalizer and invisible trash scrubber,
|
|
178
|
-
* configurable to kill whatever ghosts you want dead.
|
|
179
|
-
*
|
|
180
|
-
* ⚠️ Note: This is plain-text normalization/filtering — not an HTML/XSS sanitizer.
|
|
181
|
-
*
|
|
182
|
-
* @param {string} text
|
|
183
|
-
* @param {boolean} [purgeInvisibleChars=false] - Remove ZWSP, NBSP, bidi, etc.
|
|
184
|
-
* @param {boolean} [purgeEmojis=false] - Remove emoji characters entirely.
|
|
185
|
-
* @param {boolean} [nukeControls=false] - Remove non-whitespace control characters.
|
|
186
|
-
* @param {boolean} [keyboardOnlyFilter=false] - Keep printable ASCII + full emoji sequences only (drops other Unicode).
|
|
187
|
-
* @param {boolean} [normalizeNewlines=false] - Convert all newlines to `\n`.
|
|
188
|
-
* @param {boolean} [trimSpacesAroundNewlines=false] - Remove spaces/tabs around newlines.
|
|
189
|
-
* @param {boolean} [collapseNewLines=false] - Collapse `\n` runs (optionally preserve paragraphs).
|
|
190
|
-
* @param {boolean} [preserveParagraphs=false] - Preserve paragraph breaks when collapsing newlines.
|
|
191
|
-
* @param {boolean} [collapseSpaces=false] - Collapse multiple spaces into one.
|
|
192
|
-
* @param {boolean} [finalTrim=false] - `.trim()` the final output (head/tail).
|
|
193
|
-
* @param {number} [maxLength=Infinity] - Hard input length cap (UTF-16 code units via `text.length`).
|
|
194
|
-
* @param {'throw'|'truncate'|'noop'} [onMaxLength='throw'] - Behavior when `text.length` exceeds `maxLength`:
|
|
195
|
-
* - `'throw'`: throw a `RangeError`
|
|
196
|
-
* - `'truncate'`: slice to `maxLength` before processing
|
|
197
|
-
* - `'noop'`: return the original input unchanged (skips processing)
|
|
198
|
-
* @returns {string}
|
|
199
|
-
* @throws {TypeError} If `text` is not a string.
|
|
200
|
-
* @throws {RangeError} If input exceeds `maxLength` and `onMaxLength` is `'throw'`.
|
|
201
|
-
*/
|
|
202
|
-
export function sanctifyText(
|
|
203
|
-
text,
|
|
204
|
-
purgeInvisibleChars = false,
|
|
205
|
-
purgeEmojis = false,
|
|
206
|
-
nukeControls = false,
|
|
207
|
-
keyboardOnlyFilter = false,
|
|
208
|
-
normalizeNewlines = false,
|
|
209
|
-
trimSpacesAroundNewlines = false,
|
|
210
|
-
collapseNewLines = false,
|
|
211
|
-
preserveParagraphs = false,
|
|
212
|
-
collapseSpaces = false,
|
|
213
|
-
finalTrim = false,
|
|
214
|
-
maxLength = Infinity,
|
|
215
|
-
onMaxLength = 'throw'
|
|
216
|
-
) {
|
|
217
|
-
if (typeof text !== 'string') {
|
|
218
|
-
throw new TypeError('sanctifyText expects a string input.');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (onMaxLength !== 'truncate' && onMaxLength !== 'noop' && onMaxLength !== 'throw') {
|
|
222
|
-
onMaxLength = 'throw';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (text.length > maxLength) {
|
|
226
|
-
switch (onMaxLength) {
|
|
227
|
-
case 'truncate':
|
|
228
|
-
text = text.slice(0, maxLength);
|
|
229
|
-
break;
|
|
230
|
-
case 'noop':
|
|
231
|
-
return text;
|
|
232
|
-
case 'throw':
|
|
233
|
-
default:
|
|
234
|
-
throw new RangeError(
|
|
235
|
-
`sanctifyText input length ${text.length} exceeds maxLength ${maxLength}.`
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let cleaned = text;
|
|
241
|
-
|
|
242
|
-
// Purge invisible Unicode trash (zero-width, non-breaking, bidi junk, etc.)
|
|
243
|
-
if (purgeInvisibleChars) cleaned = purgeInvisibleTrash(cleaned);
|
|
244
|
-
|
|
245
|
-
// Remove emojis
|
|
246
|
-
if (purgeEmojis) cleaned = purgeEmojiCharacters(cleaned);
|
|
247
|
-
|
|
248
|
-
// Nuke control characters (excluding whitespace)
|
|
249
|
-
if (nukeControls) cleaned = purgeControlCharacters(cleaned);
|
|
250
|
-
|
|
251
|
-
// Keep only ASCII/emojis
|
|
252
|
-
if (keyboardOnlyFilter) cleaned = purgeNonKeyboardChars(cleaned, purgeEmojis);
|
|
253
|
-
|
|
254
|
-
// Normalize line endings to Unix style (\n)
|
|
255
|
-
if (normalizeNewlines) cleaned = normalizeNewlineChars(cleaned);
|
|
256
|
-
|
|
257
|
-
// Remove spaces/tabs around newlines
|
|
258
|
-
if (trimSpacesAroundNewlines) cleaned = trimSpacesAroundNewlineChars(cleaned);
|
|
259
|
-
|
|
260
|
-
// Collapse excessive newlines, Optionally preserve Paragraphs
|
|
261
|
-
if (collapseNewLines) cleaned = collapseMultipleNewLines(cleaned, preserveParagraphs);
|
|
262
|
-
|
|
263
|
-
// Collapse multiple spaces into a single space
|
|
264
|
-
if (collapseSpaces) cleaned = collapseExtraSpaces(cleaned);
|
|
265
|
-
|
|
266
|
-
// Final trim, return Sanctified Text
|
|
267
|
-
return finalTrim ? cleaned.trim() : cleaned;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// --- Micro helpers ---
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
/**
|
|
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
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
.replace(
|
|
378
|
-
.replace(
|
|
379
|
-
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
// *
|
|
403
|
-
// * -
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
*
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1
|
+
// src/sanctifyText.js
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} SanctifyOptions
|
|
6
|
+
* @property {boolean} [purgeInvisibleChars]
|
|
7
|
+
* @property {boolean} [purgeEmojis]
|
|
8
|
+
* @property {boolean} [nukeControls]
|
|
9
|
+
* @property {boolean} [keyboardOnlyFilter]
|
|
10
|
+
* @property {boolean} [normalizeNewlines]
|
|
11
|
+
* @property {boolean} [trimSpacesAroundNewlines]
|
|
12
|
+
* @property {boolean} [collapseNewLines]
|
|
13
|
+
* @property {boolean} [preserveParagraphs]
|
|
14
|
+
* @property {boolean} [collapseSpaces]
|
|
15
|
+
* @property {boolean} [finalTrim]
|
|
16
|
+
* @property {number} [maxLength]
|
|
17
|
+
* @property {'throw'|'truncate'|'noop'} [onMaxLength]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Summons a customized sanctifier function with pre-bound booleans.
|
|
23
|
+
*
|
|
24
|
+
* Accepts full flag names and returns a text-cleaning function.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} [defaultOptions={}]
|
|
27
|
+
* @param {boolean} [defaultOptions.purgeInvisibleChars]
|
|
28
|
+
* @param {boolean} [defaultOptions.purgeEmojis]
|
|
29
|
+
* @param {boolean} [defaultOptions.nukeControls]
|
|
30
|
+
* @param {boolean} [defaultOptions.keyboardOnlyFilter]
|
|
31
|
+
* @param {boolean} [defaultOptions.normalizeNewlines]
|
|
32
|
+
* @param {boolean} [defaultOptions.trimSpacesAroundNewlines]
|
|
33
|
+
* @param {boolean} [defaultOptions.collapseNewLines]
|
|
34
|
+
* @param {boolean} [defaultOptions.preserveParagraphs]
|
|
35
|
+
* @param {boolean} [defaultOptions.collapseSpaces]
|
|
36
|
+
* @param {boolean} [defaultOptions.finalTrim]
|
|
37
|
+
* @param {number} [defaultOptions.maxLength=Infinity] - Hard input length cap (UTF-16 code units).
|
|
38
|
+
* @param {'throw' | 'truncate' | 'noop'} [defaultOptions.onMaxLength='throw'] - Behavior when input exceeds maxLength.
|
|
39
|
+
* @returns {(text: string) => string}
|
|
40
|
+
*/
|
|
41
|
+
export function summonSanctifier(defaultOptions = {}) {
|
|
42
|
+
const purgeInvisibleChars = !!defaultOptions.purgeInvisibleChars;
|
|
43
|
+
const purgeEmojis = !!defaultOptions.purgeEmojis;
|
|
44
|
+
const nukeControls = !!defaultOptions.nukeControls;
|
|
45
|
+
const keyboardOnlyFilter = !!defaultOptions.keyboardOnlyFilter;
|
|
46
|
+
const normalizeNewlines = !!defaultOptions.normalizeNewlines;
|
|
47
|
+
const trimSpacesAroundNewlines = !!defaultOptions.trimSpacesAroundNewlines;
|
|
48
|
+
const collapseNewLines = !!defaultOptions.collapseNewLines;
|
|
49
|
+
const preserveParagraphs = !!defaultOptions.preserveParagraphs;
|
|
50
|
+
const collapseSpaces = !!defaultOptions.collapseSpaces;
|
|
51
|
+
const finalTrim = !!defaultOptions.finalTrim;
|
|
52
|
+
|
|
53
|
+
const maxLength =
|
|
54
|
+
Number.isFinite(defaultOptions.maxLength) && defaultOptions.maxLength >= 0
|
|
55
|
+
? defaultOptions.maxLength
|
|
56
|
+
: Infinity;
|
|
57
|
+
|
|
58
|
+
const onMaxLength =
|
|
59
|
+
defaultOptions.onMaxLength === 'truncate' ||
|
|
60
|
+
defaultOptions.onMaxLength === 'noop' ||
|
|
61
|
+
defaultOptions.onMaxLength === 'throw'
|
|
62
|
+
? defaultOptions.onMaxLength
|
|
63
|
+
: 'throw';
|
|
64
|
+
|
|
65
|
+
return (text) =>
|
|
66
|
+
sanctifyText(
|
|
67
|
+
text,
|
|
68
|
+
purgeInvisibleChars,
|
|
69
|
+
purgeEmojis,
|
|
70
|
+
nukeControls,
|
|
71
|
+
keyboardOnlyFilter,
|
|
72
|
+
normalizeNewlines,
|
|
73
|
+
trimSpacesAroundNewlines,
|
|
74
|
+
collapseNewLines,
|
|
75
|
+
preserveParagraphs,
|
|
76
|
+
collapseSpaces,
|
|
77
|
+
finalTrim,
|
|
78
|
+
maxLength,
|
|
79
|
+
onMaxLength
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Added Presets ---
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Strict sanitizer:
|
|
87
|
+
* - Purge emojis
|
|
88
|
+
* - Collapse all newlines
|
|
89
|
+
* - Collapse spaces
|
|
90
|
+
* - Nuke control characters
|
|
91
|
+
*/
|
|
92
|
+
/** @param {string} text @returns {string} */
|
|
93
|
+
summonSanctifier.strict = text => sanctifyText(
|
|
94
|
+
text,
|
|
95
|
+
true, // purgeInvisibleChars
|
|
96
|
+
true, // purgeEmojis
|
|
97
|
+
true, // nukeControls
|
|
98
|
+
false, // keyboardOnlyFilter
|
|
99
|
+
true, // normalizeNewlines
|
|
100
|
+
true, // trimSpacesAroundNewlines
|
|
101
|
+
true, // collapseNewLines
|
|
102
|
+
false, // preserveParagraphs
|
|
103
|
+
true, // collapseSpaces
|
|
104
|
+
true // finalTrim
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Loose sanitizer:
|
|
110
|
+
* - Collapse spaces
|
|
111
|
+
* - Preserve paragraphs
|
|
112
|
+
* - Normalize newlines
|
|
113
|
+
*/
|
|
114
|
+
/** @param {string} text @returns {string} */
|
|
115
|
+
summonSanctifier.loose = text => sanctifyText(
|
|
116
|
+
text,
|
|
117
|
+
false, // purgeInvisibleChars
|
|
118
|
+
false, // purgeEmojis
|
|
119
|
+
false, // nukeControls
|
|
120
|
+
false, // keyboardOnlyFilter
|
|
121
|
+
true, // normalizeNewlines
|
|
122
|
+
true, // trimSpacesAroundNewlines
|
|
123
|
+
true, // collapseNewLines
|
|
124
|
+
true, // preserveParagraphs
|
|
125
|
+
true, // collapseSpaces
|
|
126
|
+
true // finalTrim
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Keyboard-only (with emojis):
|
|
132
|
+
* - Keeps emojis and printable ASCII
|
|
133
|
+
* - Strips non-standard characters
|
|
134
|
+
* - Normalizes typographic trash
|
|
135
|
+
*/
|
|
136
|
+
/** @param {string} text @returns {string} */
|
|
137
|
+
summonSanctifier.keyboardOnlyEmoji = text => sanctifyText(
|
|
138
|
+
text,
|
|
139
|
+
false, // purgeInvisibleChars
|
|
140
|
+
false, // purgeEmojis
|
|
141
|
+
false, // nukeControls
|
|
142
|
+
true, // keyboardOnlyFilter
|
|
143
|
+
true, // normalizeNewlines
|
|
144
|
+
true, // trimSpacesAroundNewlines
|
|
145
|
+
false, // collapseNewLines
|
|
146
|
+
false, // preserveParagraphs
|
|
147
|
+
false, // collapseSpaces
|
|
148
|
+
true // finalTrim
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Keyboard-only (strict):
|
|
154
|
+
* - Removes emojis
|
|
155
|
+
* - Collapses all whitespace
|
|
156
|
+
* - Restricts to printable ASCII only
|
|
157
|
+
*/
|
|
158
|
+
/** @param {string} text @returns {string} */
|
|
159
|
+
summonSanctifier.keyboardOnly = text => sanctifyText(
|
|
160
|
+
text,
|
|
161
|
+
true, // purgeInvisibleChars
|
|
162
|
+
true, // purgeEmojis
|
|
163
|
+
true, // nukeControls
|
|
164
|
+
true, // keyboardOnlyFilter
|
|
165
|
+
true, // normalizeNewlines
|
|
166
|
+
true, // trimSpacesAroundNewlines
|
|
167
|
+
true, // collapseNewLines
|
|
168
|
+
false, // preserveParagraphs
|
|
169
|
+
true, // collapseSpaces
|
|
170
|
+
true // finalTrim
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Text Sanctifier
|
|
176
|
+
*
|
|
177
|
+
* Brutal text normalizer and invisible trash scrubber,
|
|
178
|
+
* configurable to kill whatever ghosts you want dead.
|
|
179
|
+
*
|
|
180
|
+
* ⚠️ Note: This is plain-text normalization/filtering — not an HTML/XSS sanitizer.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} text
|
|
183
|
+
* @param {boolean} [purgeInvisibleChars=false] - Remove ZWSP, NBSP, bidi, tags, variation selectors, etc.
|
|
184
|
+
* @param {boolean} [purgeEmojis=false] - Remove emoji characters entirely.
|
|
185
|
+
* @param {boolean} [nukeControls=false] - Remove non-whitespace control characters.
|
|
186
|
+
* @param {boolean} [keyboardOnlyFilter=false] - Keep printable ASCII + full emoji sequences only (drops other Unicode).
|
|
187
|
+
* @param {boolean} [normalizeNewlines=false] - Convert all newlines to `\n`.
|
|
188
|
+
* @param {boolean} [trimSpacesAroundNewlines=false] - Remove spaces/tabs around newlines.
|
|
189
|
+
* @param {boolean} [collapseNewLines=false] - Collapse `\n` runs (optionally preserve paragraphs).
|
|
190
|
+
* @param {boolean} [preserveParagraphs=false] - Preserve paragraph breaks when collapsing newlines.
|
|
191
|
+
* @param {boolean} [collapseSpaces=false] - Collapse multiple spaces into one.
|
|
192
|
+
* @param {boolean} [finalTrim=false] - `.trim()` the final output (head/tail).
|
|
193
|
+
* @param {number} [maxLength=Infinity] - Hard input length cap (UTF-16 code units via `text.length`).
|
|
194
|
+
* @param {'throw'|'truncate'|'noop'} [onMaxLength='throw'] - Behavior when `text.length` exceeds `maxLength`:
|
|
195
|
+
* - `'throw'`: throw a `RangeError`
|
|
196
|
+
* - `'truncate'`: slice to `maxLength` before processing
|
|
197
|
+
* - `'noop'`: return the original input unchanged (skips processing)
|
|
198
|
+
* @returns {string}
|
|
199
|
+
* @throws {TypeError} If `text` is not a string.
|
|
200
|
+
* @throws {RangeError} If input exceeds `maxLength` and `onMaxLength` is `'throw'`.
|
|
201
|
+
*/
|
|
202
|
+
export function sanctifyText(
|
|
203
|
+
text,
|
|
204
|
+
purgeInvisibleChars = false,
|
|
205
|
+
purgeEmojis = false,
|
|
206
|
+
nukeControls = false,
|
|
207
|
+
keyboardOnlyFilter = false,
|
|
208
|
+
normalizeNewlines = false,
|
|
209
|
+
trimSpacesAroundNewlines = false,
|
|
210
|
+
collapseNewLines = false,
|
|
211
|
+
preserveParagraphs = false,
|
|
212
|
+
collapseSpaces = false,
|
|
213
|
+
finalTrim = false,
|
|
214
|
+
maxLength = Infinity,
|
|
215
|
+
onMaxLength = 'throw'
|
|
216
|
+
) {
|
|
217
|
+
if (typeof text !== 'string') {
|
|
218
|
+
throw new TypeError('sanctifyText expects a string input.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (onMaxLength !== 'truncate' && onMaxLength !== 'noop' && onMaxLength !== 'throw') {
|
|
222
|
+
onMaxLength = 'throw';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (text.length > maxLength) {
|
|
226
|
+
switch (onMaxLength) {
|
|
227
|
+
case 'truncate':
|
|
228
|
+
text = text.slice(0, maxLength);
|
|
229
|
+
break;
|
|
230
|
+
case 'noop':
|
|
231
|
+
return text;
|
|
232
|
+
case 'throw':
|
|
233
|
+
default:
|
|
234
|
+
throw new RangeError(
|
|
235
|
+
`sanctifyText input length ${text.length} exceeds maxLength ${maxLength}.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let cleaned = text;
|
|
241
|
+
|
|
242
|
+
// Purge invisible Unicode trash (zero-width, non-breaking, bidi junk, etc.)
|
|
243
|
+
if (purgeInvisibleChars) cleaned = purgeInvisibleTrash(cleaned);
|
|
244
|
+
|
|
245
|
+
// Remove emojis
|
|
246
|
+
if (purgeEmojis) cleaned = purgeEmojiCharacters(cleaned);
|
|
247
|
+
|
|
248
|
+
// Nuke control characters (excluding whitespace)
|
|
249
|
+
if (nukeControls) cleaned = purgeControlCharacters(cleaned);
|
|
250
|
+
|
|
251
|
+
// Keep only ASCII/emojis
|
|
252
|
+
if (keyboardOnlyFilter) cleaned = purgeNonKeyboardChars(cleaned, purgeEmojis);
|
|
253
|
+
|
|
254
|
+
// Normalize line endings to Unix style (\n)
|
|
255
|
+
if (normalizeNewlines) cleaned = normalizeNewlineChars(cleaned);
|
|
256
|
+
|
|
257
|
+
// Remove spaces/tabs around newlines
|
|
258
|
+
if (trimSpacesAroundNewlines) cleaned = trimSpacesAroundNewlineChars(cleaned);
|
|
259
|
+
|
|
260
|
+
// Collapse excessive newlines, Optionally preserve Paragraphs
|
|
261
|
+
if (collapseNewLines) cleaned = collapseMultipleNewLines(cleaned, preserveParagraphs);
|
|
262
|
+
|
|
263
|
+
// Collapse multiple spaces into a single space
|
|
264
|
+
if (collapseSpaces) cleaned = collapseExtraSpaces(cleaned);
|
|
265
|
+
|
|
266
|
+
// Final trim, return Sanctified Text
|
|
267
|
+
return finalTrim ? cleaned.trim() : cleaned;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// --- Micro helpers ---
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
/**
|
|
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"
|
|
285
|
+
*
|
|
286
|
+
* @param {string} text
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
export const INVISIBLE_TRASH_REGEX = /[ ͏ᅟᅠ -- -- ㅤ︀-️ᅠ\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}]+/gu;
|
|
290
|
+
function purgeInvisibleTrash(text) {
|
|
291
|
+
return text.replace(INVISIBLE_TRASH_REGEX, '');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Extracts and preserves complete emoji sequences from a string.
|
|
297
|
+
*
|
|
298
|
+
* Uses the global `EMOJI_REGEX` to match full emoji grapheme sequences,
|
|
299
|
+
* including:
|
|
300
|
+
* - Extended pictographic characters
|
|
301
|
+
* - Zero-width joiner (ZWJ) sequences (e.g. 👨👩👧👦)
|
|
302
|
+
* - Variation Selector-16 (VS16) emoji presentation forms (e.g. ✌️, ‼️)
|
|
303
|
+
*
|
|
304
|
+
* Any non-emoji characters are ignored.
|
|
305
|
+
*
|
|
306
|
+
* Important:
|
|
307
|
+
* `EMOJI_REGEX` is global (`/g`), so `lastIndex` is reset before matching
|
|
308
|
+
* to ensure deterministic behavior across repeated calls.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} text - Input string potentially containing emoji sequences.
|
|
311
|
+
* @returns {string} A string containing only the matched emoji sequences,
|
|
312
|
+
* concatenated in original order.
|
|
313
|
+
*/
|
|
314
|
+
function extractEmojiSequences(text) {
|
|
315
|
+
EMOJI_REGEX.lastIndex = 0;
|
|
316
|
+
if (!EMOJI_REGEX.test(text)) return ''; // quick reject
|
|
317
|
+
EMOJI_REGEX.lastIndex = 0;
|
|
318
|
+
|
|
319
|
+
let out = '';
|
|
320
|
+
for (const match of text.matchAll(EMOJI_REGEX)) out += match[0];
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Matches any character that is NOT:
|
|
326
|
+
* - Printable ASCII (U+0020–U+007E)
|
|
327
|
+
* - Newline characters: \n (LF), \r (CR)
|
|
328
|
+
* This allows for \n, \r, and \r\n line endings.
|
|
329
|
+
*/
|
|
330
|
+
export const ASCII_KEYBOARD_SAFE_REGEX = /[^\x20-\x7E\r\n]+/gu;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Removes all non-keyboard characters, preserving:
|
|
334
|
+
* - Printable ASCII (0x20–0x7E)
|
|
335
|
+
* - Emojis (Unicode Emoji_Presentation + FE0F variants)
|
|
336
|
+
*/
|
|
337
|
+
function purgeNonKeyboardChars(text, purgeEmojis = false) {
|
|
338
|
+
const normalized = normalizeTypographicJank(text);
|
|
339
|
+
|
|
340
|
+
// If emojis are being purged, keyboard-only becomes "ASCII + CR/LF only"
|
|
341
|
+
if (purgeEmojis) {
|
|
342
|
+
return normalized.replace(ASCII_KEYBOARD_SAFE_REGEX, '');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Replace each non-ASCII run with ONLY the emoji sequences inside it.
|
|
346
|
+
// This preserves ZWJ sequences and VS16 variants (e.g. 👨👩👧👦, ✌️, ‼️),
|
|
347
|
+
// while dropping non-emoji non-ASCII (e.g. 𝌆).
|
|
348
|
+
return normalized.replace(ASCII_KEYBOARD_SAFE_REGEX, (run) => extractEmojiSequences(run));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
// Smart apostrophes (single quote variations)
|
|
353
|
+
const SMART_SINGLE_QUOTES_REGEX = /[\u2018\u2019\u201A\u201B\u2032\u2035]/g;
|
|
354
|
+
|
|
355
|
+
// Smart double quotes (including primes and guillemets)
|
|
356
|
+
const SMART_DOUBLE_QUOTES_REGEX = /[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g;
|
|
357
|
+
|
|
358
|
+
// Dash variants: en dash, em dash, horizontal bar, figure dash, minus
|
|
359
|
+
const UNICODE_DASHES_REGEX = /[\u2012\u2013\u2014\u2015\u2212]/g;
|
|
360
|
+
|
|
361
|
+
// Ellipsis character
|
|
362
|
+
const ELLIPSIS_REGEX = /\u2026/g;
|
|
363
|
+
|
|
364
|
+
// Bullets and middle dot
|
|
365
|
+
const BULLETS_REGEX = /[\u2022\u00B7]/g;
|
|
366
|
+
|
|
367
|
+
// Full-width ASCII punctuation: U+FF01 - U+FF5E
|
|
368
|
+
const FULLWIDTH_PUNCTUATION_REGEX = /[\uFF01-\uFF5E]/g;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Normalizes typographic Unicode punctuation into ASCII equivalents.
|
|
372
|
+
* @param {string} text
|
|
373
|
+
* @returns {string}
|
|
374
|
+
*/
|
|
375
|
+
export function normalizeTypographicJank(text) {
|
|
376
|
+
return text
|
|
377
|
+
.replace(SMART_SINGLE_QUOTES_REGEX, "'")
|
|
378
|
+
.replace(SMART_DOUBLE_QUOTES_REGEX, '"')
|
|
379
|
+
.replace(UNICODE_DASHES_REGEX, '-')
|
|
380
|
+
.replace(ELLIPSIS_REGEX, '...')
|
|
381
|
+
.replace(BULLETS_REGEX, '*')
|
|
382
|
+
.replace(FULLWIDTH_PUNCTUATION_REGEX, m =>
|
|
383
|
+
String.fromCharCode(m.charCodeAt(0) - 0xFEE0)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
/** @type {RegExp} */
|
|
389
|
+
export let EMOJI_REGEX;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Try Unicode property escape regex (preferred).
|
|
393
|
+
* Fallback to basic emoji ranges if unsupported.
|
|
394
|
+
*/
|
|
395
|
+
try {
|
|
396
|
+
EMOJI_REGEX = new RegExp(
|
|
397
|
+
'(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)',
|
|
398
|
+
'gu'
|
|
399
|
+
);
|
|
400
|
+
} catch {
|
|
401
|
+
// Fallback: wide-range emoji component match (flags, tones, symbols)
|
|
402
|
+
// * Covers:
|
|
403
|
+
// * - Emoji base chars (1F300–1FAFF)
|
|
404
|
+
// * - Dingbats (2700–27BF) like ❌, ✅, ☑️
|
|
405
|
+
// * - Skin tones (1F3FB–1F3FF)
|
|
406
|
+
// * - ZWJ (200D), variation selectors (FE0F)
|
|
407
|
+
// * - Regional indicators (1F1E6–1F1FF)
|
|
408
|
+
EMOJI_REGEX = /[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Removes all emoji characters using Unicode property escapes.
|
|
413
|
+
* Supports modern environments (Unicode v13+) with fallback.
|
|
414
|
+
*
|
|
415
|
+
* @param {string} text
|
|
416
|
+
* @returns {string}
|
|
417
|
+
*/
|
|
418
|
+
function purgeEmojiCharacters(text) {
|
|
419
|
+
return text.replace(EMOJI_REGEX, '');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Normalizes all line endings to a consistent format.
|
|
425
|
+
*
|
|
426
|
+
* Converts:
|
|
427
|
+
* - Windows ("\r\n"), Old Mac ("\r"), Unix ("\n")
|
|
428
|
+
* Into the specified newline format (default: Unix "\n").
|
|
429
|
+
*
|
|
430
|
+
* @param {string} text - Input string to normalize.
|
|
431
|
+
* @param {string} [normalized='\n'] - Target newline style (e.g. '\n', '\r\n').
|
|
432
|
+
* @returns {string}
|
|
433
|
+
*/
|
|
434
|
+
const NORMALIZE_NEWLINES_REGEX = /\r\n|\r|\n/g;
|
|
435
|
+
function normalizeNewlineChars(text, normalized = '\n') {
|
|
436
|
+
return text.replace(NORMALIZE_NEWLINES_REGEX, normalized);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Trims spaces and tabs immediately before and after newlines.
|
|
442
|
+
*
|
|
443
|
+
* Example: "hello \n world" → "hello\nworld"
|
|
444
|
+
*
|
|
445
|
+
* Preserves the newline itself, just removes whitespace around it.
|
|
446
|
+
*
|
|
447
|
+
* @param {string} text
|
|
448
|
+
* @returns {string}
|
|
449
|
+
*/
|
|
450
|
+
const TRIM_SPACES_AROUND_NEWLINES_REGEX = /[ \t]*(\n+)[ \t]*/g;
|
|
451
|
+
function trimSpacesAroundNewlineChars(text) {
|
|
452
|
+
return text.replace(TRIM_SPACES_AROUND_NEWLINES_REGEX, '$1');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Collapses excessive newlines based on paragraph preservation settings.
|
|
458
|
+
*
|
|
459
|
+
* - If preserveParagraphs = true:
|
|
460
|
+
* Collapses 3 or more consecutive newlines → exactly two ("\n\n")
|
|
461
|
+
* - If preserveParagraphs = false:
|
|
462
|
+
* Collapses 2 or more consecutive newlines → exactly one ("\n")
|
|
463
|
+
*
|
|
464
|
+
* Example (preserveParagraphs = true):
|
|
465
|
+
* "Line1\n\n\n\nLine2" → "Line1\n\nLine2"
|
|
466
|
+
*
|
|
467
|
+
* @param {string} text
|
|
468
|
+
* @param {boolean} preserveParagraphs - Whether to preserve paragraph breaks (double newlines).
|
|
469
|
+
* @returns {string}
|
|
470
|
+
*/
|
|
471
|
+
const MULTIPLE_NEWLINES_REGEX = /\n{2,}/g;
|
|
472
|
+
const TRIPLE_NEWLINES_REGEX = /\n{3,}/g;
|
|
473
|
+
|
|
474
|
+
function collapseMultipleNewLines(text, preserveParagraphs) {
|
|
475
|
+
return preserveParagraphs
|
|
476
|
+
? text.replace(TRIPLE_NEWLINES_REGEX, '\n\n')
|
|
477
|
+
: text.replace(MULTIPLE_NEWLINES_REGEX, '\n');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Collapses multiple consecutive spaces into a single space.
|
|
483
|
+
* (Does not touch tabs or newlines.)
|
|
484
|
+
*
|
|
485
|
+
* Example: "hello world" → "hello world"
|
|
486
|
+
*
|
|
487
|
+
* @param {string} text
|
|
488
|
+
* @returns {string}
|
|
489
|
+
*/
|
|
490
|
+
const MULTIPLE_SPACES_REGEX = / {2,}/g;
|
|
491
|
+
function collapseExtraSpaces(text) {
|
|
492
|
+
return text.replace(MULTIPLE_SPACES_REGEX, ' ');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Nukes hidden control characters that are invisible and often dangerous.
|
|
498
|
+
* (Excludes necessary whitespace like \n and \t.)
|
|
499
|
+
*
|
|
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)
|
|
504
|
+
*
|
|
505
|
+
* @param {string} text
|
|
506
|
+
* @returns {string}
|
|
507
|
+
*/
|
|
508
|
+
export const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u0080-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g;
|
|
509
|
+
function purgeControlCharacters(text) {
|
|
510
|
+
return text.replace(CONTROL_CHARS_REGEX, '');
|
|
511
|
+
}
|