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.
- package/README.md +81 -19
- package/bin/text-sanctifier.js +48 -7
- package/dist/text-sanctifier.min.js +6 -15
- package/dist/types/hiddenText.d.ts +62 -6
- package/dist/types/index.d.ts +9 -1
- package/dist/types/reveal.d.ts +26 -0
- package/dist/types/sanctifyText.d.ts +156 -91
- package/dist/types/textPatterns.d.ts +15 -0
- package/dist/types/version.generated.d.ts +2 -0
- package/package.json +79 -80
- package/src/hiddenText.js +129 -26
- package/src/index.d.ts +9 -1
- package/src/index.js +9 -2
- package/src/inspectText.js +2 -8
- package/src/reveal.js +40 -0
- package/src/sanctifyText.js +179 -125
- package/src/textPatterns.js +78 -0
- package/src/version.generated.js +5 -0
package/src/sanctifyText.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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();
|