text-sanctifier 1.1.0 → 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/index.d.ts +7 -0
- package/dist/types/inspectText.d.ts +13 -2
- package/dist/types/sanctifyText.d.ts +8 -4
- package/package.json +8 -3
- package/src/index.d.ts +7 -0
- package/src/index.js +9 -0
- package/src/inspectText.js +386 -339
- package/src/sanctifyText.js +511 -507
package/src/inspectText.js
CHANGED
|
@@ -1,340 +1,387 @@
|
|
|
1
|
-
// ./src/inspectText.js
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* @
|
|
16
|
-
* @property {boolean}
|
|
17
|
-
* @property {
|
|
18
|
-
* @property {boolean}
|
|
19
|
-
* @property {
|
|
20
|
-
* @property {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
flag(stableTest(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
129
|
-
* - `
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* @
|
|
136
|
-
* @property {number}
|
|
137
|
-
* @property {
|
|
138
|
-
* @property {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* @
|
|
146
|
-
* @property {number}
|
|
147
|
-
* @property {number}
|
|
148
|
-
* @property {number}
|
|
149
|
-
* @property {number}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* @
|
|
154
|
-
* @property {
|
|
155
|
-
* @property {
|
|
156
|
-
* @property {number}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* @
|
|
162
|
-
* @property {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (c <
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (c
|
|
188
|
-
if (c ===
|
|
189
|
-
if (c
|
|
190
|
-
if (c ===
|
|
191
|
-
if (c ===
|
|
192
|
-
return null;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
*
|
|
210
|
-
* @param {
|
|
211
|
-
* @
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (c ===
|
|
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
|
-
|
|
1
|
+
// ./src/inspectText.js
|
|
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
|
+
|
|
11
|
+
/** @typedef {import('./sanctifyText.js').SanctifyOptions} SanctifyOptions */
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} UnicodeTrashReport
|
|
16
|
+
* @property {boolean} hasControlChars
|
|
17
|
+
* @property {boolean} hasInvisibleChars
|
|
18
|
+
* @property {boolean} hasMixedNewlines
|
|
19
|
+
* @property {'LF'|'CRLF'|'CR'|'Mixed'|null} newlineStyle
|
|
20
|
+
* @property {boolean} hasEmojis
|
|
21
|
+
* @property {boolean} hasNonKeyboardChars
|
|
22
|
+
* @property {string[]} summary
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Safe `.test()` for global/sticky regexes.
|
|
28
|
+
* Global regexes mutate `lastIndex`, which makes `.test()` unreliable across calls.
|
|
29
|
+
* @param {RegExp} re
|
|
30
|
+
* @param {string} s
|
|
31
|
+
*/
|
|
32
|
+
function stableTest(re, s) {
|
|
33
|
+
re.lastIndex = 0;
|
|
34
|
+
return re.test(s);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if text contains any non-keyboard characters (excluding emojis),
|
|
39
|
+
* after typographic normalization.
|
|
40
|
+
*
|
|
41
|
+
* "Keyboard" here means:
|
|
42
|
+
* - Printable ASCII (0x20–0x7E)
|
|
43
|
+
* - CR/LF
|
|
44
|
+
* - Emojis (per EMOJI_REGEX)
|
|
45
|
+
*
|
|
46
|
+
* @param {string} text
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function hasNonKeyboardCharsExcludingEmoji(text) {
|
|
50
|
+
const normalized = normalizeTypographicJank(text);
|
|
51
|
+
|
|
52
|
+
// Fast path: no non-ascii runs => no non-keyboard chars
|
|
53
|
+
if (!stableTest(ASCII_KEYBOARD_SAFE_REGEX, normalized)) return false;
|
|
54
|
+
|
|
55
|
+
// Walk code points so we can distinguish emoji from other non-ascii safely
|
|
56
|
+
for (const ch of normalized) {
|
|
57
|
+
// allowed: ASCII printable + newlines
|
|
58
|
+
if (ch === '\n' || ch === '\r' || (ch >= ' ' && ch <= '~')) continue;
|
|
59
|
+
|
|
60
|
+
// allowed: emoji
|
|
61
|
+
if (stableTest(EMOJI_REGEX, ch)) continue;
|
|
62
|
+
|
|
63
|
+
// anything else non-ascii is "non-keyboard"
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detects textual "trash" or anomalies in a given string.
|
|
72
|
+
* @param {string} text @returns {UnicodeTrashReport}
|
|
73
|
+
*/
|
|
74
|
+
export function inspectText(text) {
|
|
75
|
+
if (typeof text !== 'string') {
|
|
76
|
+
throw new TypeError('inspectText expects a string input.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const summary = [];
|
|
80
|
+
const report = {
|
|
81
|
+
hasControlChars: false,
|
|
82
|
+
hasInvisibleChars: false,
|
|
83
|
+
hasMixedNewlines: false,
|
|
84
|
+
newlineStyle: null,
|
|
85
|
+
hasEmojis: false,
|
|
86
|
+
hasNonKeyboardChars: false,
|
|
87
|
+
summary,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const flag = (condition, key, message) => {
|
|
91
|
+
if (condition) {
|
|
92
|
+
report[key] = true;
|
|
93
|
+
summary.push(message);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// === Pattern Checks ===
|
|
98
|
+
flag(stableTest(CONTROL_CHARS_REGEX, text), 'hasControlChars', 'Control characters detected.');
|
|
99
|
+
flag(stableTest(INVISIBLE_TRASH_REGEX, text), 'hasInvisibleChars', 'Invisible Unicode characters detected.');
|
|
100
|
+
flag(stableTest(EMOJI_REGEX, text), 'hasEmojis', 'Emojis detected.');
|
|
101
|
+
|
|
102
|
+
// === Newline Analysis ===
|
|
103
|
+
const { mixed, types } = getNewlineStats(text);
|
|
104
|
+
report.hasMixedNewlines = mixed;
|
|
105
|
+
report.newlineStyle = mixed ? 'Mixed' : types[0] || null;
|
|
106
|
+
|
|
107
|
+
if (report.newlineStyle) {
|
|
108
|
+
summary.push(
|
|
109
|
+
mixed ? 'Mixed newline styles detected.' : `Consistent newline style: ${report.newlineStyle}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// === Non-keyboard characters (excluding emojis) ===
|
|
114
|
+
flag(
|
|
115
|
+
hasNonKeyboardCharsExcludingEmoji(text),
|
|
116
|
+
'hasNonKeyboardChars',
|
|
117
|
+
'Non-keyboard characters detected.'
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return report;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @typedef {'bidi_control'|'zero_width'|'nonbreaking_space'|'bom'|'nul'|'c0_control'|'c1_control'|'invisible_format'|'hangul_filler'|'variation_selector'|'tag'} TrashCategory
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One located character.
|
|
129
|
+
* - `offset`: 0-based UTF-16 index into the input (a JS string index); for a code point
|
|
130
|
+
* above U+FFFF, the index of its high surrogate.
|
|
131
|
+
* - `line`: 1-based; lines end at LF, CR, or CRLF (counted once).
|
|
132
|
+
* - `column`: 1-based, in UTF-16 code units from the start of the line.
|
|
133
|
+
* - `atStart`: present on `bom` findings only; true at offset 0, where a BOM is legitimate.
|
|
134
|
+
*
|
|
135
|
+
* @typedef {Object} TrashFinding
|
|
136
|
+
* @property {number} offset
|
|
137
|
+
* @property {number} line
|
|
138
|
+
* @property {number} column
|
|
139
|
+
* @property {number} codePoint
|
|
140
|
+
* @property {TrashCategory} category
|
|
141
|
+
* @property {boolean} [atStart]
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @typedef {Object} TrashCounts
|
|
146
|
+
* @property {number} bidi_control
|
|
147
|
+
* @property {number} zero_width
|
|
148
|
+
* @property {number} nonbreaking_space
|
|
149
|
+
* @property {number} bom
|
|
150
|
+
* @property {number} nul
|
|
151
|
+
* @property {number} c0_control
|
|
152
|
+
* @property {number} c1_control
|
|
153
|
+
* @property {number} invisible_format
|
|
154
|
+
* @property {number} hangul_filler
|
|
155
|
+
* @property {number} variation_selector
|
|
156
|
+
* @property {number} tag
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @typedef {Object} DetailedTrashReport
|
|
161
|
+
* @property {TrashFinding[]} findings - In offset order, at most `maxFindings` long.
|
|
162
|
+
* @property {TrashCounts} counts - Every occurrence per category, listed or not.
|
|
163
|
+
* @property {number} total - Sum of `counts`.
|
|
164
|
+
* @property {boolean} truncated - True when `total` exceeds `findings.length`.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @typedef {Object} InspectDetailedOptions
|
|
169
|
+
* @property {number} [maxFindings=100] - Cap on listed findings (non-negative integer or Infinity).
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
const DEFAULT_MAX_FINDINGS = 100;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Category of one BMP code point (one UTF-16 code unit), or null when it is not trash.
|
|
176
|
+
* Tab, LF and CR are whitespace, not trash. DEL (U+007F) is counted with the C0 controls.
|
|
177
|
+
* Surrogates return null; the caller pairs them and uses classifyAstral.
|
|
178
|
+
* @param {number} c
|
|
179
|
+
* @returns {TrashCategory|null}
|
|
180
|
+
*/
|
|
181
|
+
function classifyCodeUnit(c) {
|
|
182
|
+
if (c < 0x80) {
|
|
183
|
+
if (c === 0) return 'nul';
|
|
184
|
+
if (c < 0x20) return c === 0x09 || c === 0x0A || c === 0x0D ? null : 'c0_control';
|
|
185
|
+
return c === 0x7F ? 'c0_control' : null;
|
|
186
|
+
}
|
|
187
|
+
if (c <= 0x9F) return 'c1_control';
|
|
188
|
+
if (c === 0xA0) return 'nonbreaking_space';
|
|
189
|
+
if (c === 0xAD || c === 0x034F || c === 0x180E) return 'invisible_format';
|
|
190
|
+
if (c === 0x061C) return 'bidi_control';
|
|
191
|
+
if (c === 0x115F || c === 0x1160) return 'hangul_filler';
|
|
192
|
+
if (c < 0x2000) return null;
|
|
193
|
+
if (c <= 0x200A) return 'nonbreaking_space';
|
|
194
|
+
if (c <= 0x200D) return 'zero_width';
|
|
195
|
+
if (c <= 0x200F) return 'bidi_control';
|
|
196
|
+
if (c >= 0x202A && c <= 0x202E) return 'bidi_control';
|
|
197
|
+
if (c === 0x202F) return 'nonbreaking_space';
|
|
198
|
+
if (c === 0x2060) return 'zero_width';
|
|
199
|
+
if (c >= 0x2061 && c <= 0x2064) return 'invisible_format';
|
|
200
|
+
if (c >= 0x2066 && c <= 0x2069) return 'bidi_control';
|
|
201
|
+
if (c === 0x3000) return 'nonbreaking_space';
|
|
202
|
+
if (c === 0x3164 || c === 0xFFA0) return 'hangul_filler';
|
|
203
|
+
if (c >= 0xFE00 && c <= 0xFE0F) return 'variation_selector';
|
|
204
|
+
if (c === 0xFEFF) return 'bom';
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Category of a code point above U+FFFF, or null when it is not trash.
|
|
210
|
+
* @param {number} cp
|
|
211
|
+
* @returns {TrashCategory|null}
|
|
212
|
+
*/
|
|
213
|
+
function classifyAstral(cp) {
|
|
214
|
+
if (cp >= 0xE0000 && cp <= 0xE007F) return 'tag';
|
|
215
|
+
if (cp >= 0xE0100 && cp <= 0xE01EF) return 'variation_selector';
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** High surrogate of U+E0000–U+E03FF, the only astral block with trash (tags, VS17–VS256). */
|
|
220
|
+
const TRASH_HIGH_SURROGATE = 0xDB40;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Locates invisible and control characters, one finding per character.
|
|
224
|
+
*
|
|
225
|
+
* Categories:
|
|
226
|
+
* - `bidi_control`: U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069
|
|
227
|
+
* - `zero_width`: U+200B–U+200D, U+2060
|
|
228
|
+
* - `nonbreaking_space`: non-breaking and typographic spaces U+00A0, U+2000–U+200A, U+202F, U+3000
|
|
229
|
+
* - `bom`: U+FEFF (`atStart` says whether it sits at offset 0)
|
|
230
|
+
* - `nul`: U+0000
|
|
231
|
+
* - `c0_control`: U+0001–U+001F except tab, LF and CR; and U+007F
|
|
232
|
+
* - `c1_control`: U+0080–U+009F
|
|
233
|
+
* - `invisible_format`: soft hyphen U+00AD, U+034F, U+180E, invisible operators U+2061–U+2064
|
|
234
|
+
* - `hangul_filler`: U+115F, U+1160, U+3164, U+FFA0 (letters that render blank)
|
|
235
|
+
* - `variation_selector`: U+FE00–U+FE0F, U+E0100–U+E01EF (including VS16 in emoji)
|
|
236
|
+
* - `tag`: U+E0000–U+E007F (tag characters, used for "ASCII smuggling")
|
|
237
|
+
*
|
|
238
|
+
* `codePoint` is the full code point; `offset` and `column` count UTF-16 units.
|
|
239
|
+
* Every occurrence is counted; only the first `maxFindings` are listed.
|
|
240
|
+
* One pass over the string, no regex, no copy.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} text
|
|
243
|
+
* @param {InspectDetailedOptions} [options]
|
|
244
|
+
* @returns {DetailedTrashReport}
|
|
245
|
+
*/
|
|
246
|
+
export function inspectTextDetailed(text, options = {}) {
|
|
247
|
+
if (typeof text !== 'string') {
|
|
248
|
+
throw new TypeError('inspectTextDetailed expects a string input.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const requested = options && options.maxFindings;
|
|
252
|
+
const maxFindings =
|
|
253
|
+
requested === Infinity || (Number.isInteger(requested) && requested >= 0)
|
|
254
|
+
? requested
|
|
255
|
+
: DEFAULT_MAX_FINDINGS;
|
|
256
|
+
|
|
257
|
+
/** @type {TrashCounts} */
|
|
258
|
+
const counts = {
|
|
259
|
+
bidi_control: 0,
|
|
260
|
+
zero_width: 0,
|
|
261
|
+
nonbreaking_space: 0,
|
|
262
|
+
bom: 0,
|
|
263
|
+
nul: 0,
|
|
264
|
+
c0_control: 0,
|
|
265
|
+
c1_control: 0,
|
|
266
|
+
invisible_format: 0,
|
|
267
|
+
hangul_filler: 0,
|
|
268
|
+
variation_selector: 0,
|
|
269
|
+
tag: 0,
|
|
270
|
+
};
|
|
271
|
+
/** @type {TrashFinding[]} */
|
|
272
|
+
const findings = [];
|
|
273
|
+
let total = 0;
|
|
274
|
+
let line = 1;
|
|
275
|
+
let lineStart = 0;
|
|
276
|
+
|
|
277
|
+
for (let i = 0, n = text.length; i < n; i++) {
|
|
278
|
+
const c = text.charCodeAt(i);
|
|
279
|
+
|
|
280
|
+
// Printable ASCII: the common case
|
|
281
|
+
if (c >= 0x20 && c < 0x7F) continue;
|
|
282
|
+
|
|
283
|
+
if (c === 0x0A) {
|
|
284
|
+
line++;
|
|
285
|
+
lineStart = i + 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (c === 0x0D) {
|
|
289
|
+
// CRLF ends the line at its LF
|
|
290
|
+
if (text.charCodeAt(i + 1) !== 0x0A) {
|
|
291
|
+
line++;
|
|
292
|
+
lineStart = i + 1;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const offset = i;
|
|
298
|
+
let codePoint = c;
|
|
299
|
+
let category;
|
|
300
|
+
if (c === TRASH_HIGH_SURROGATE) {
|
|
301
|
+
const low = text.charCodeAt(i + 1);
|
|
302
|
+
if (!(low >= 0xDC00 && low <= 0xDFFF)) continue; // lone surrogate
|
|
303
|
+
codePoint = 0x10000 + ((c - 0xD800) << 10) + (low - 0xDC00);
|
|
304
|
+
category = classifyAstral(codePoint);
|
|
305
|
+
i++; // the low surrogate is part of this code point
|
|
306
|
+
} else {
|
|
307
|
+
category = classifyCodeUnit(c);
|
|
308
|
+
}
|
|
309
|
+
if (category === null) continue;
|
|
310
|
+
|
|
311
|
+
counts[category]++;
|
|
312
|
+
total++;
|
|
313
|
+
if (findings.length < maxFindings) {
|
|
314
|
+
/** @type {TrashFinding} */
|
|
315
|
+
const finding = { offset, line, column: offset - lineStart + 1, codePoint, category };
|
|
316
|
+
if (category === 'bom') finding.atStart = offset === 0;
|
|
317
|
+
findings.push(finding);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { findings, counts, total, truncated: total > findings.length };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Counts the number of different newline types in a string.
|
|
326
|
+
* @param {string} text
|
|
327
|
+
* @returns {{
|
|
328
|
+
* crlf: number,
|
|
329
|
+
* cr: number,
|
|
330
|
+
* lf: number,
|
|
331
|
+
* types: string[],
|
|
332
|
+
* mixed: boolean
|
|
333
|
+
* }}
|
|
334
|
+
*/
|
|
335
|
+
export function getNewlineStats(text) {
|
|
336
|
+
if (typeof text !== 'string') {
|
|
337
|
+
throw new TypeError('getNewlineStats expects a string input.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// One counting pass: a CR followed by LF is one CRLF; any other CR or LF stands alone.
|
|
341
|
+
let crlf = 0;
|
|
342
|
+
let cr = 0;
|
|
343
|
+
let lf = 0;
|
|
344
|
+
|
|
345
|
+
for (let i = 0, n = text.length; i < n; i++) {
|
|
346
|
+
const c = text.charCodeAt(i);
|
|
347
|
+
if (c === 0x0D) {
|
|
348
|
+
if (text.charCodeAt(i + 1) === 0x0A) {
|
|
349
|
+
crlf++;
|
|
350
|
+
i++;
|
|
351
|
+
} else {
|
|
352
|
+
cr++;
|
|
353
|
+
}
|
|
354
|
+
} else if (c === 0x0A) {
|
|
355
|
+
lf++;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const count = { crlf, cr, lf };
|
|
360
|
+
|
|
361
|
+
const types = [];
|
|
362
|
+
if (count.crlf > 0) types.push('CRLF');
|
|
363
|
+
if (count.cr > 0) types.push('CR');
|
|
364
|
+
if (count.lf > 0) types.push('LF');
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
...count,
|
|
368
|
+
types,
|
|
369
|
+
mixed: types.length > 1,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Creates defaultOptions for summonSanctifier based on inspectText result
|
|
375
|
+
* @param {!UnicodeTrashReport} report
|
|
376
|
+
* @return {!SanctifyOptions}
|
|
377
|
+
*/
|
|
378
|
+
export function getRecommendedSanctifierOptions(report) {
|
|
379
|
+
return {
|
|
380
|
+
purgeInvisibleChars: report.hasInvisibleChars,
|
|
381
|
+
purgeEmojis: report.hasEmojis,
|
|
382
|
+
nukeControls: report.hasControlChars,
|
|
383
|
+
keyboardOnlyFilter: report.hasNonKeyboardChars,
|
|
384
|
+
normalizeNewlines:
|
|
385
|
+
report.hasMixedNewlines || report.newlineStyle === 'CRLF' || report.newlineStyle === 'CR',
|
|
386
|
+
};
|
|
340
387
|
}
|