text-sanctifier 1.3.10 → 1.3.11
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 +13 -8
- package/bin/text-sanctifier.js +123 -35
- package/dist/text-sanctifier.min.js +5 -5
- package/dist/types/inspectText.d.ts +4 -1
- package/dist/types/sanctifyText.d.ts +1 -1
- package/dist/types/version.generated.d.ts +1 -1
- package/package.json +7 -5
- package/src/inspectText.js +7 -3
- package/src/sanctifyText.js +17 -11
- package/src/version.generated.js +1 -1
- package/src/index.d.ts +0 -22
package/README.md
CHANGED
|
@@ -106,20 +106,22 @@ Used by `summonSanctifier(options)`. All boolean options default to `false`. Pro
|
|
|
106
106
|
| `collapseSpaces` | Collapse runs of ordinary spaces to one; keep tabs and newlines. |
|
|
107
107
|
| `finalTrim` | Apply JavaScript `.trim()` to the result. |
|
|
108
108
|
| `maxLength` | Input cap in UTF-16 code units, checked before processing. Default `Infinity`; negative, non-finite or non-number values mean no cap. |
|
|
109
|
-
| `onMaxLength` | Default `'throw'`: throw `RangeError` above the cap. `'truncate'`: slice input to the cap before processing
|
|
109
|
+
| `onMaxLength` | Default `'throw'`: throw `RangeError` above the cap. `'truncate'`: slice input to at most the cap before processing, never through a surrogate pair. `'noop'`: return original input unchanged. Invalid values use `'throw'`. |
|
|
110
110
|
|
|
111
111
|
```js
|
|
112
112
|
import { summonSanctifier } from 'text-sanctifier';
|
|
113
113
|
const clean = summonSanctifier({ purgeInvisibleChars: true, maxLength: 10000, onMaxLength: 'throw' });
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
Every sanitizer replaces unpaired surrogates with U+FFFD before anything else, so removing a character between two halves cannot join them into a new one.
|
|
117
|
+
|
|
116
118
|
### Errors
|
|
117
119
|
|
|
118
120
|
All text-processing exports throw `TypeError` on non-string input, including the preset functions, the sanitizer returned by `summonSanctifier`, `findHiddenRuns` and `decodeTagText`. Inputs are not coerced to strings. Sanitizers also throw `RangeError` when input exceeds `maxLength` with `onMaxLength: 'throw'`.
|
|
119
121
|
|
|
120
122
|
### Presets as standalone imports
|
|
121
123
|
|
|
122
|
-
Each preset is also its own export: `sanctifyStrict`, `sanctifyLoose`, `sanctifyKeyboardOnly`, `sanctifyKeyboardOnlyEmoji` and `sanctifyPasted`. They are the same functions as `summonSanctifier.strict` and the rest. Import one directly and a bundler leaves out `summonSanctifier` and the other presets (about <!-- gen:size:sanctifyPasted -->1.1 KiB<!-- /gen --> gzipped instead of <!-- gen:size:summonSanctifier.pasted -->1.
|
|
124
|
+
Each preset is also its own export: `sanctifyStrict`, `sanctifyLoose`, `sanctifyKeyboardOnly`, `sanctifyKeyboardOnlyEmoji` and `sanctifyPasted`. They are the same functions as `summonSanctifier.strict` and the rest. Import one directly and a bundler leaves out `summonSanctifier` and the other presets (about <!-- gen:size:sanctifyPasted -->1.1 KiB<!-- /gen --> gzipped instead of <!-- gen:size:summonSanctifier.pasted -->1.5 KiB<!-- /gen -->).
|
|
123
125
|
|
|
124
126
|
```js
|
|
125
127
|
import { sanctifyPasted } from 'text-sanctifier';
|
|
@@ -187,7 +189,7 @@ const report = inspectText(input);
|
|
|
187
189
|
*/
|
|
188
190
|
```
|
|
189
191
|
|
|
190
|
-
Use `inspectText` to preflight text content before rendering, storing, or linting. It's a diagnostic tool to help inform sanitization needs. Pass the report to `getRecommendedSanctifierOptions(report)` to auto-generate config flags for `summonSanctifier()`.
|
|
192
|
+
Use `inspectText` to preflight text content before rendering, storing, or linting. It's a diagnostic tool to help inform sanitization needs. Pass the report to `getRecommendedSanctifierOptions(report)` to auto-generate config flags for `summonSanctifier()`. It turns on what removes trash without losing meaning; `purgeEmojis` and `keyboardOnlyFilter` delete content, so you opt in to those yourself.
|
|
191
193
|
|
|
192
194
|
### `inspectTextDetailed(text: string, options?: { maxFindings?: number }): DetailedTrashReport`
|
|
193
195
|
|
|
@@ -299,6 +301,7 @@ The package ships a `text-sanctifier` command for CI and coding agents, which ca
|
|
|
299
301
|
```sh
|
|
300
302
|
npx text-sanctifier --help
|
|
301
303
|
npx text-sanctifier --version
|
|
304
|
+
npx text-sanctifier check
|
|
302
305
|
npx text-sanctifier check src docs "**/*.md"
|
|
303
306
|
npx text-sanctifier fix --dry-run src
|
|
304
307
|
npx text-sanctifier reveal notes.md
|
|
@@ -361,13 +364,13 @@ The repository publishes a [pre-commit](https://pre-commit.com) hook manifest, s
|
|
|
361
364
|
# .pre-commit-config.yaml
|
|
362
365
|
repos:
|
|
363
366
|
- repo: https://github.com/iWhatty/text-sanctifier
|
|
364
|
-
rev: v1.3.
|
|
367
|
+
rev: v1.3.11 # a release tag
|
|
365
368
|
hooks:
|
|
366
369
|
- id: text-sanctifier # every staged text file
|
|
367
370
|
- id: text-sanctifier-ai-config # agent instruction files, with --ai-config
|
|
368
371
|
```
|
|
369
372
|
|
|
370
|
-
- `text-sanctifier` runs `text-sanctifier check` on staged text files. Use `exclude:` for files that legitimately contain these characters, or `args: [--strip-bom]` and the other `check` options.
|
|
373
|
+
- `text-sanctifier` runs `text-sanctifier check` on staged text files, as pre-commit defines them: it can drop a file with control bytes near its start before the hook sees it, so scan the repository with `check` as well if that matters to you. Use `exclude:` for files that legitimately contain these characters, or `args: [--strip-bom]` and the other `check` options.
|
|
371
374
|
- `text-sanctifier-ai-config` runs `text-sanctifier check --ai-config` on staged files that match the `--ai-config` file list.
|
|
372
375
|
- A hook fails the commit when it finds something (exit code 1). Neither hook changes files; run `text-sanctifier fix` yourself.
|
|
373
376
|
|
|
@@ -400,9 +403,11 @@ git diff | npx text-sanctifier reveal
|
|
|
400
403
|
|
|
401
404
|
- Paths can be files, directories or globs (`*`, `**`, `?`, `[...]`, `{a,b}`). Quote globs so the shell passes them through.
|
|
402
405
|
- Inside a git work tree, directories and globs list files with `git ls-files`, so anything `.gitignore` excludes is skipped. Outside git, `.git` and `node_modules` are skipped. A file named explicitly is always read.
|
|
403
|
-
- Binary files
|
|
406
|
+
- Binary files are skipped, counted in the summary and listed under `skipped` in JSON. A file counts as binary when it is not valid UTF-8 and its first 8000 bytes hold a NUL and more than 5% control bytes, so a stray NUL or bad byte does not hide a text file: the NUL is reported as `nul`. Stdin and AI configuration files are never treated as binary. `fix` is more careful, because it rewrites files: in a directory or glob it also skips any file with a NUL in those bytes, and it cleans such a file only if you name it.
|
|
407
|
+
- UTF-16 files with a BOM are decoded and checked. `fix` never rewrites them and lists them under `skipped`.
|
|
404
408
|
- Files that are not valid UTF-8 are still checked, with each invalid byte read as U+FFFD, so one bad byte cannot hide the rest of the file. `check` notes them on stderr and lists them under `notUtf8` in JSON; `fix` never writes them and lists them under `skipped`.
|
|
405
|
-
-
|
|
409
|
+
- `-` reads stdin. With no paths, `check` reads piped input, and scans the current directory when stdin is a terminal, the null device or empty. `check --ai-config` always searches the current directory with no paths. `fix` and `reveal` read piped input. `fix` writes the cleaned text to stdout and the report to stderr.
|
|
410
|
+
- A path or glob that matches no files, or a run that checks nothing, exits 2 instead of reporting a clean result. So does `fix` when it refuses stdin or a file you named. Files it skips in a directory or glob don't fail the run.
|
|
406
411
|
|
|
407
412
|
### Exit codes
|
|
408
413
|
|
|
@@ -410,7 +415,7 @@ git diff | npx text-sanctifier reveal
|
|
|
410
415
|
| --- | --- | --- | --- |
|
|
411
416
|
| 0 | nothing found | done (or nothing to do) | done |
|
|
412
417
|
| 1 | findings | `--dry-run` with changes pending | not used |
|
|
413
|
-
| 2 | usage error, missing path or
|
|
418
|
+
| 2 | usage error, missing or unreadable input, a path matching no files, or nothing checked | same, or an input you named that it refused to rewrite | same as `check` |
|
|
414
419
|
|
|
415
420
|
---
|
|
416
421
|
|
package/bin/text-sanctifier.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Plain Node (>=18), no dependencies. Exit codes: 0 clean, 1 findings (check,
|
|
11
11
|
// or fix --dry-run with changes pending), 2 usage or I/O error.
|
|
12
12
|
|
|
13
|
-
import { readFileSync, writeFileSync, statSync,
|
|
13
|
+
import { readFileSync, writeFileSync, statSync, fstatSync, readdirSync } from 'node:fs';
|
|
14
14
|
import { resolve, join, sep, relative } from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
@@ -32,8 +32,12 @@ Usage:
|
|
|
32
32
|
text-sanctifier reveal [paths...]
|
|
33
33
|
|
|
34
34
|
Paths may be files, directories or globs (quote globs so the shell leaves them
|
|
35
|
-
alone). "-"
|
|
36
|
-
|
|
35
|
+
alone). "-" reads stdin. With no paths, check reads piped input, or scans the current
|
|
36
|
+
directory when stdin is a terminal, the null device or empty. Fix and reveal read
|
|
37
|
+
piped input. Directories and globs skip files ignored by git (inside
|
|
38
|
+
a git work tree). Binary files are skipped; files that are text apart from a stray
|
|
39
|
+
NUL or bad byte are not, and UTF-16 files with a BOM are decoded. A path or glob that
|
|
40
|
+
matches no files, or a run that checks nothing, exits 2.
|
|
37
41
|
|
|
38
42
|
check options:
|
|
39
43
|
--format F text (default), json or sarif (SARIF 2.1.0)
|
|
@@ -77,6 +81,7 @@ const BOM = 0xFEFF;
|
|
|
77
81
|
const STDIN = '-';
|
|
78
82
|
const STDIN_LABEL = '<stdin>';
|
|
79
83
|
const BINARY_SNIFF_BYTES = 8000;
|
|
84
|
+
const BINARY_CONTROL_SHARE = 0.05;
|
|
80
85
|
const WALK_SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
81
86
|
const CASE_INSENSITIVE_FS = process.platform === 'win32' || process.platform === 'darwin';
|
|
82
87
|
|
|
@@ -206,21 +211,36 @@ function isGitWorkTree(cwd) {
|
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
/**
|
|
209
|
-
* Lists tracked and untracked-but-not-ignored files under
|
|
214
|
+
* Lists tracked and untracked-but-not-ignored files under the given pathspecs.
|
|
210
215
|
* Returns null when git cannot answer (for example a path outside the work tree).
|
|
211
216
|
* @param {string} cwd
|
|
212
|
-
* @param {string}
|
|
217
|
+
* @param {string[]} pathspecs
|
|
213
218
|
* @returns {string[]|null}
|
|
214
219
|
*/
|
|
215
|
-
function gitListFiles(cwd,
|
|
220
|
+
function gitListFiles(cwd, pathspecs) {
|
|
216
221
|
const r = spawnSync(
|
|
217
222
|
'git',
|
|
218
|
-
['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--',
|
|
223
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ...pathspecs],
|
|
219
224
|
{ cwd, encoding: 'utf8', maxBuffer: 1 << 28 }
|
|
220
225
|
);
|
|
221
226
|
if (r.error || r.status !== 0) return null;
|
|
222
|
-
// --cached lists tracked files deleted from the work tree
|
|
223
|
-
|
|
227
|
+
// --cached also lists tracked files deleted from the work tree, submodules and
|
|
228
|
+
// symlinks to directories; keep only what can be read as a file
|
|
229
|
+
return r.stdout.split('\0').filter((f) => {
|
|
230
|
+
if (!f) return false;
|
|
231
|
+
try { return statSync(resolve(cwd, f)).isFile(); } catch { return false; }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Expands `{a,b}` into one glob per alternative: git pathspecs have no brace syntax.
|
|
237
|
+
* @param {string} glob
|
|
238
|
+
* @returns {string[]}
|
|
239
|
+
*/
|
|
240
|
+
function expandBraces(glob) {
|
|
241
|
+
const m = /\{([^{}]*)\}/.exec(glob);
|
|
242
|
+
if (!m) return [glob];
|
|
243
|
+
return m[1].split(',').flatMap((alt) => expandBraces(glob.slice(0, m.index) + alt + glob.slice(m.index + m[0].length)));
|
|
224
244
|
}
|
|
225
245
|
|
|
226
246
|
/**
|
|
@@ -366,7 +386,7 @@ function collectInputs(args, cwd, warn, aiConfig = false) {
|
|
|
366
386
|
|
|
367
387
|
if (inGit === undefined) inGit = isGitWorkTree(cwd);
|
|
368
388
|
let listed =
|
|
369
|
-
(inGit && gitListFiles(cwd, isGlob ? `:(glob)${
|
|
389
|
+
(inGit && gitListFiles(cwd, isGlob ? expandBraces(toSlash(arg)).map((g) => `:(glob)${g}`) : [toSlash(arg)])) ||
|
|
370
390
|
(isGlob ? walkGlob(cwd, arg) : walk(cwd, arg));
|
|
371
391
|
|
|
372
392
|
if (aiConfig && !isGlob) {
|
|
@@ -374,6 +394,7 @@ function collectInputs(args, cwd, warn, aiConfig = false) {
|
|
|
374
394
|
listed = listed.filter((f) => isAiConfigPath(toSlash(relative(base, resolve(cwd, f)))));
|
|
375
395
|
} else if (listed.length === 0) {
|
|
376
396
|
warn(`${arg}: no files matched`);
|
|
397
|
+
errors++;
|
|
377
398
|
}
|
|
378
399
|
for (const f of listed) add(f, false);
|
|
379
400
|
}
|
|
@@ -385,24 +406,57 @@ function collectInputs(args, cwd, warn, aiConfig = false) {
|
|
|
385
406
|
|
|
386
407
|
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
387
408
|
const utf8Lenient = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
409
|
+
const utf16 = new TextDecoder('utf-16le', { ignoreBOM: true });
|
|
388
410
|
|
|
389
411
|
/**
|
|
390
|
-
*
|
|
391
|
-
* BINARY_SNIFF_BYTES
|
|
392
|
-
* and set `lossy`, so one bad byte cannot hide the rest of a file from `check`.
|
|
412
|
+
* A NUL plus a share of control bytes that text does not have, in the first
|
|
413
|
+
* BINARY_SNIFF_BYTES. One stray NUL is not enough, or it would hide a file from `check`.
|
|
393
414
|
* @param {Uint8Array} bytes
|
|
415
|
+
*/
|
|
416
|
+
function looksBinary(bytes) {
|
|
417
|
+
const sample = bytes.subarray(0, BINARY_SNIFF_BYTES);
|
|
418
|
+
if (!sample.includes(0)) return false;
|
|
419
|
+
let controls = 0;
|
|
420
|
+
for (const b of sample) if (b < 9 || (b > 13 && b < 32)) controls++;
|
|
421
|
+
return controls / sample.length > BINARY_CONTROL_SHARE;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Decodes UTF-8, or UTF-16 with a BOM (setting `wide`), keeping a leading BOM. With
|
|
426
|
+
* `sniff`, data that is not UTF-8 and looks binary is not decoded. Other bytes that are
|
|
427
|
+
* not UTF-8 become U+FFFD and set `lossy`, so one bad byte cannot hide the rest of a file.
|
|
428
|
+
* @param {Buffer} bytes
|
|
394
429
|
* @param {boolean} sniff
|
|
395
|
-
* @returns {{ text: string|null, reason?: string, lossy?: boolean }}
|
|
430
|
+
* @returns {{ text: string|null, reason?: string, lossy?: boolean, wide?: boolean }}
|
|
396
431
|
*/
|
|
397
432
|
function decode(bytes, sniff) {
|
|
398
|
-
if (
|
|
433
|
+
if (bytes[0] === 0xFF && bytes[1] === 0xFE) return { text: utf16.decode(bytes), wide: true };
|
|
434
|
+
if (bytes[0] === 0xFE && bytes[1] === 0xFF) {
|
|
435
|
+
return { text: utf16.decode(Buffer.from(bytes.subarray(0, bytes.length & ~1)).swap16()), wide: true };
|
|
436
|
+
}
|
|
399
437
|
try {
|
|
400
438
|
return { text: utf8.decode(bytes) };
|
|
401
|
-
} catch {
|
|
439
|
+
} catch (e) {
|
|
440
|
+
if (!(e instanceof TypeError)) throw e; // too large for a string, not bad UTF-8
|
|
441
|
+
if (sniff && looksBinary(bytes)) return { text: null, reason: 'binary' };
|
|
402
442
|
return { text: utf8Lenient.decode(bytes), lossy: true };
|
|
403
443
|
}
|
|
404
444
|
}
|
|
405
445
|
|
|
446
|
+
/**
|
|
447
|
+
* True when stdin may hold input for check with no paths: anything but a terminal,
|
|
448
|
+
* the null device or an empty file. Pipes must be read to detect empty input;
|
|
449
|
+
* Windows pipes do not report isFIFO().
|
|
450
|
+
*/
|
|
451
|
+
function stdinHasInput() {
|
|
452
|
+
try {
|
|
453
|
+
const stat = fstatSync(0);
|
|
454
|
+
return !stat.isCharacterDevice() && !(stat.isFile() && stat.size === 0);
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
406
460
|
/** @returns {Promise<Buffer>} */
|
|
407
461
|
function readStdin() {
|
|
408
462
|
return new Promise((res, rej) => {
|
|
@@ -656,11 +710,23 @@ async function main(argv) {
|
|
|
656
710
|
if (opts.version) { out(VERSION); return 0; }
|
|
657
711
|
|
|
658
712
|
const cwd = process.cwd();
|
|
713
|
+
const warn = (msg) => err(`text-sanctifier: ${msg}`);
|
|
659
714
|
let paths = opts.paths;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
715
|
+
/** @type {Buffer | undefined} */
|
|
716
|
+
let stdinBytes;
|
|
717
|
+
// check with no paths reads nonempty stdin once, otherwise scans the current
|
|
718
|
+
// directory. AI configuration discovery always defaults to the directory.
|
|
719
|
+
if (paths.length === 0 && opts.command === 'check') {
|
|
663
720
|
paths = ['.'];
|
|
721
|
+
if (!opts.aiConfig && stdinHasInput()) {
|
|
722
|
+
try {
|
|
723
|
+
stdinBytes = await readStdin();
|
|
724
|
+
} catch (e) {
|
|
725
|
+
warn(`${STDIN_LABEL}: ${e.code || e.message}`);
|
|
726
|
+
return 2;
|
|
727
|
+
}
|
|
728
|
+
if (stdinBytes.length > 0) paths = [STDIN];
|
|
729
|
+
}
|
|
664
730
|
} else if (paths.length === 0) {
|
|
665
731
|
if (process.stdin.isTTY) {
|
|
666
732
|
err('text-sanctifier: no paths given and nothing piped on stdin.\nRun "text-sanctifier --help" for usage.');
|
|
@@ -669,7 +735,6 @@ async function main(argv) {
|
|
|
669
735
|
paths = [STDIN];
|
|
670
736
|
}
|
|
671
737
|
|
|
672
|
-
const warn = (msg) => err(`text-sanctifier: ${msg}`);
|
|
673
738
|
const { inputs, errors: pathErrors } = collectInputs(paths, cwd, warn, opts.aiConfig);
|
|
674
739
|
let errors = pathErrors;
|
|
675
740
|
/** @type {Array<{ path: string, reason: string }>} */
|
|
@@ -682,11 +747,12 @@ async function main(argv) {
|
|
|
682
747
|
* @param {(label: string, text: string, input: { path: string, explicit: boolean }) => void} visit
|
|
683
748
|
*/
|
|
684
749
|
const forEachText = async (visit) => {
|
|
750
|
+
let visited = 0;
|
|
685
751
|
for (const input of inputs) {
|
|
686
752
|
const label = input.path === STDIN ? STDIN_LABEL : input.path;
|
|
687
753
|
let bytes;
|
|
688
754
|
try {
|
|
689
|
-
bytes = input.path === STDIN ? await readStdin() : readFileSync(resolve(cwd, input.path));
|
|
755
|
+
bytes = input.path === STDIN ? (stdinBytes ?? await readStdin()) : readFileSync(resolve(cwd, input.path));
|
|
690
756
|
} catch (e) {
|
|
691
757
|
warn(`${label}: ${e.code || e.message}`);
|
|
692
758
|
errors++;
|
|
@@ -694,26 +760,45 @@ async function main(argv) {
|
|
|
694
760
|
}
|
|
695
761
|
// Files that look binary are skipped. Stdin and AI configuration files are text
|
|
696
762
|
// by intent, so a NUL in one is a finding, not a reason to skip it.
|
|
697
|
-
|
|
698
|
-
|
|
763
|
+
let decoded;
|
|
764
|
+
try {
|
|
765
|
+
decoded = decode(bytes, input.path !== STDIN && !opts.aiConfig);
|
|
766
|
+
} catch (e) {
|
|
767
|
+
warn(`${label}: ${e.message}`);
|
|
768
|
+
errors++;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const { text, lossy, wide } = decoded;
|
|
772
|
+
let { reason } = decoded;
|
|
773
|
+
// fix writes UTF-8 in place, so it never rewrites what it could not decode or
|
|
774
|
+
// what was not UTF-8. A directory scan also leaves any file with a NUL alone;
|
|
775
|
+
// a file you name is cleaned if it is otherwise UTF-8.
|
|
776
|
+
if (opts.command === 'fix' && !reason) {
|
|
777
|
+
if (lossy) reason = 'not UTF-8';
|
|
778
|
+
else if (wide) reason = 'UTF-16';
|
|
779
|
+
else if (!input.explicit && bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) reason = 'binary';
|
|
780
|
+
}
|
|
781
|
+
if (reason) {
|
|
699
782
|
skipped.push({ path: label, reason });
|
|
700
|
-
if (input.explicit) warn(`${label}: skipped (${reason})`);
|
|
783
|
+
if (input.explicit || reason !== 'binary') warn(`${label}: skipped (${reason})`);
|
|
784
|
+
// Asked to clean this input and did not: that must not look like success
|
|
785
|
+
if (opts.command === 'fix' && input.explicit) errors++;
|
|
701
786
|
continue;
|
|
702
787
|
}
|
|
703
788
|
if (lossy) {
|
|
704
|
-
// fix never rewrites bytes it could not decode
|
|
705
|
-
if (opts.command === 'fix') {
|
|
706
|
-
skipped.push({ path: label, reason: 'not UTF-8' });
|
|
707
|
-
warn(`${label}: skipped (not UTF-8)`);
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
789
|
notUtf8.push(label);
|
|
711
790
|
warn(`${label}: not valid UTF-8; checked with invalid bytes read as U+FFFD`);
|
|
712
791
|
}
|
|
792
|
+
visited++;
|
|
713
793
|
visit(label, text, input);
|
|
714
794
|
}
|
|
795
|
+
// A clean result must mean something was read. Finding no AI configuration files is
|
|
796
|
+
// an answer; --ai-config reports that itself.
|
|
797
|
+
if (visited === 0 && errors === 0 && !opts.aiConfig) errors++;
|
|
715
798
|
};
|
|
716
799
|
|
|
800
|
+
const skippedNote = () => (skipped.length > 0 ? `; ${plural(skipped.length, 'file')} skipped` : '');
|
|
801
|
+
|
|
717
802
|
if (opts.command === 'check') {
|
|
718
803
|
const files = [];
|
|
719
804
|
let scanned = 0;
|
|
@@ -765,8 +850,9 @@ async function main(argv) {
|
|
|
765
850
|
out('No AI configuration files found.');
|
|
766
851
|
if (opts.paths.length === 0 && !process.stdin.isTTY) out('To check piped text, pass "-": text-sanctifier check --ai-config -');
|
|
767
852
|
}
|
|
768
|
-
else if (
|
|
769
|
-
else
|
|
853
|
+
else if (scanned === 0) out(`Nothing checked${skippedNote()}.`);
|
|
854
|
+
else if (total === 0) out(`No ${opts.aiConfig ? 'hidden text, ' : ''}invisible or control characters in ${plural(scanned, kind)}${skippedNote()}.`);
|
|
855
|
+
else out(`${plural(total, 'finding')}${opts.aiConfig ? ` (${plural(hiddenRunCount, 'hidden run')})` : ''} in ${files.length} of ${plural(scanned, kind)}${skippedNote()}.`);
|
|
770
856
|
}
|
|
771
857
|
return errors > 0 ? 2 : total > 0 ? 1 : 0;
|
|
772
858
|
}
|
|
@@ -858,9 +944,11 @@ async function main(argv) {
|
|
|
858
944
|
if (f.newlinesNormalized) parts.push('normalized newlines');
|
|
859
945
|
report(`${verb} ${f.path}: ${parts.join('; ')}`);
|
|
860
946
|
}
|
|
861
|
-
report(
|
|
862
|
-
? `Nothing
|
|
863
|
-
:
|
|
947
|
+
report(scanned === 0
|
|
948
|
+
? `Nothing checked${skippedNote()}.`
|
|
949
|
+
: files.length === 0
|
|
950
|
+
? `Nothing to fix in ${plural(scanned, 'file')}${skippedNote()}.`
|
|
951
|
+
: `${opts.dryRun ? 'Would change' : 'Changed'} ${files.length} of ${plural(scanned, 'file')}${skippedNote()}.`);
|
|
864
952
|
}
|
|
865
953
|
return errors > 0 ? 2 : opts.dryRun && files.length > 0 ? 1 : 0;
|
|
866
954
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
var y=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g,
|
|
2
|
-
`||n==="\r"||n>=" "&&n<="~")&&!m(
|
|
3
|
-
`){return e.replace(
|
|
1
|
+
var y=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g,S=/[\u00A0\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u2000-\u200F\u202A-\u202F\u205F-\u206F\u3000\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}-\u{E0FFF}]+/gu;function I(e){return e<8192?v(e):B(e)}function v(e){return e<174?e<32?e<11?e<1?"nul":e<9?"c0_control":null:e<13?"c0_control":e<14?null:"c0_control":e<160?e<127?null:e<128?"c0_control":"c1_control":e<161?"nonbreaking_space":e<173?null:"invisible_format":e<4449?e<1564?e<847?null:e<848?"invisible_format":null:e<1565?"bidi_control":e<4447?null:"hangul_filler":e<6155?e<6068?null:e<6070?"invisible_format":null:e<6159?e<6158?"variation_selector":"invisible_format":e<6160?"variation_selector":null}function B(e){return e<8304?e<8240?e<8208?e<8203?"nonbreaking_space":e<8206?"zero_width":"bidi_control":e<8234?null:e<8239?"bidi_control":"nonbreaking_space":e<8289?e<8287?null:e<8288?"nonbreaking_space":"zero_width":e<8294?"invisible_format":e<8298?"bidi_control":"invisible_format":e<65040?e<12644?e<12288?null:e<12289?"nonbreaking_space":null:e<12645?"hangul_filler":e<65024?null:"variation_selector":e<65440?e<65279?null:e<65280?"bom":null:e<65520?e<65441?"hangul_filler":null:e<65529?"invisible_format":null}function b(e){return e<917504?e<113828?e<113824?null:"invisible_format":e<119155?null:e<119163?"invisible_format":null:e<917760?e<917632?"tag":"invisible_format":e<918e3?"variation_selector":e<921600?"invisible_format":null}var g=/[^\x20-\x7E\r\n]+/gu,G=/[\u2018\u2019\u201A\u201B\u2032\u2035]/g,X=/[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g,M=/[\u2012\u2013\u2014\u2015\u2212]/g,O=/\u2026/g,k=/[\u2022\u00B7]/g,P=/[\uFF01-\uFF5E]/g;function d(e){return e.replace(G,"'").replace(X,'"').replace(M,"-").replace(O,"...").replace(k,"*").replace(P,r=>String.fromCharCode(r.charCodeAt(0)-65248))}function U(){try{return new RegExp("(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)","gu")}catch{return/[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu}}var p=U();function m(e,r){return e.lastIndex=0,e.test(r)}function z(e){let r=d(e);if(!m(g,r))return!1;for(let n of r)if(!(n===`
|
|
2
|
+
`||n==="\r"||n>=" "&&n<="~")&&!m(p,n))return!0;return!1}function j(e){if(typeof e!="string")throw new TypeError("inspectText expects a string input.");let r=[],n={hasControlChars:!1,hasInvisibleChars:!1,hasMixedNewlines:!1,newlineStyle:null,hasEmojis:!1,hasNonKeyboardChars:!1,summary:r},u=(l,c,f)=>{l&&(n[c]=!0,r.push(f))};u(m(y,e),"hasControlChars","Control characters detected."),u(m(S,e),"hasInvisibleChars","Invisible Unicode characters detected."),u(m(p,e),"hasEmojis","Emojis detected.");let{mixed:o,types:i}=R(e);return n.hasMixedNewlines=o,n.newlineStyle=o?"Mixed":i[0]||null,n.newlineStyle&&r.push(o?"Mixed newline styles detected.":`Consistent newline style: ${n.newlineStyle}`),u(z(e),"hasNonKeyboardChars","Non-keyboard characters detected."),n}var K=100;function A(e,r={}){if(typeof e!="string")throw new TypeError("inspectTextDetailed expects a string input.");let n=r&&r.maxFindings,u=n===1/0||Number.isInteger(n)&&n>=0?n:K,o={bidi_control:0,zero_width:0,nonbreaking_space:0,bom:0,nul:0,c0_control:0,c1_control:0,invisible_format:0,hangul_filler:0,variation_selector:0,tag:0},i=[],l=0,c=1,f=0;for(let a=0,h=e.length;a<h;a++){let s=e.charCodeAt(a);if(s>=32&&s<127)continue;if(s===10){c++,f=a+1;continue}if(s===13){e.charCodeAt(a+1)!==10&&(c++,f=a+1);continue}let x=a,F=s,E;if(s>=55296&&s<=56319){let t=e.charCodeAt(a+1);if(!(t>=56320&&t<=57343))continue;F=65536+(s-55296<<10)+(t-56320),E=b(F),a++}else E=I(s);if(E!==null&&(o[E]++,l++,i.length<u)){let t={offset:x,line:c,column:x-f+1,codePoint:F,category:E};E==="bom"&&(t.atStart=x===0),i.push(t)}}return{findings:i,counts:o,total:l,truncated:l>i.length}}function R(e){if(typeof e!="string")throw new TypeError("getNewlineStats expects a string input.");let r=0,n=0,u=0;for(let l=0,c=e.length;l<c;l++){let f=e.charCodeAt(l);f===13?e.charCodeAt(l+1)===10?(r++,l++):n++:f===10&&u++}let o={crlf:r,cr:n,lf:u},i=[];return o.crlf>0&&i.push("CRLF"),o.cr>0&&i.push("CR"),o.lf>0&&i.push("LF"),{...o,types:i,mixed:i.length>1}}function H(e){return{normalizeSpaces:e.hasInvisibleChars,purgeInvisibleChars:e.hasInvisibleChars,purgeEmojis:!1,nukeControls:e.hasControlChars,keyboardOnlyFilter:!1,normalizeNewlines:e.hasMixedNewlines||e.newlineStyle==="CRLF"||e.newlineStyle==="CR"}}function W(e={}){let r=!!e.normalizeSpaces,n=!!e.purgeZeroWidthSpaces,u=!!e.purgeInvisibleChars,o=!!e.purgeEmojis,i=!!e.nukeControls,l=!!e.keyboardOnlyFilter,c=!!e.normalizeNewlines,f=!!e.trimSpacesAroundNewlines,a=!!e.collapseNewLines,h=!!e.preserveParagraphs,s=!!e.collapseSpaces,x=!!e.finalTrim,F=Number.isFinite(e.maxLength)&&e.maxLength>=0?e.maxLength:1/0,E=e.onMaxLength==="truncate"||e.onMaxLength==="noop"||e.onMaxLength==="throw"?e.onMaxLength:"throw";return t=>_(t,u,o,i,l,c,f,a,h,s,x,F,E,r,n)}function w(e){return _(e,!0,!0,!0,!1,!0,!0,!0,!1,!0,!0)}function N(e){return _(e,!1,!1,!1,!1,!0,!0,!0,!0,!0,!0)}function T(e){return _(e,!1,!1,!1,!0,!0,!0,!1,!1,!1,!0)}function L(e){return _(e,!0,!0,!0,!0,!0,!0,!0,!1,!0,!0)}function D(e){return _(e,!1,!1,!1,!1,!0,!1,!1,!1,!1,!1,1/0,"throw",!0,!0)}var J=Object.assign(W,{strict:w,loose:N,keyboardOnlyEmoji:T,keyboardOnly:L,pasted:D});function _(e,r=!1,n=!1,u=!1,o=!1,i=!1,l=!1,c=!1,f=!1,a=!1,h=!1,s=1/0,x="throw",F=!1,E=!1){if(typeof e!="string")throw new TypeError("sanctifyText expects a string input.");if(e.length>s)switch(x){case"truncate":{let C=e.charCodeAt(s-1);e=e.slice(0,C>=55296&&C<=56319?s-1:s)}break;case"noop":return e;default:throw new RangeError(`sanctifyText input length ${e.length} exceeds maxLength ${s}.`)}let t=e.replace($,"\uFFFD");return F&&(t=t.replace(V," ")),E&&(t=t.replace(Z,"")),r&&(t=Y(t)),n&&(t=ee(t)),u&&(t=fe(t)),o&&(t=Q(t,n)),i&&(t=ne(t)),l&&(t=ie(t)),c&&(t=se(t,f)),a&&(t=ae(t)),h?t.trim():t}var V=/[\u00A0\u2000-\u200A\u202F\u205F]/g,$=/[\uD800-\uDFFF]/gu,Z=/[\u200B\u2060\uFEFF]/g;function Y(e){return e.replace(S,"")}function q(e){if(p.lastIndex=0,!p.test(e))return"";p.lastIndex=0;let r="";for(let n of e.matchAll(p))r+=n[0];return r}function Q(e,r=!1){let n=d(e);return r?n.replace(g,""):n.replace(g,u=>q(u))}function ee(e){return e.replace(p,"")}var re=/\r\n|\r|\n/g;function ne(e,r=`
|
|
3
|
+
`){return e.replace(re,r)}var te=/[ \t]*(\n+)[ \t]*|([ \t]+)/g;function ie(e){return e.replace(te,"$1$2")}var oe=/\n{2,}/g,ue=/\n{3,}/g;function se(e,r){return r?e.replace(ue,`
|
|
4
4
|
|
|
5
|
-
`):e.replace(
|
|
6
|
-
`)}var
|
|
5
|
+
`):e.replace(oe,`
|
|
6
|
+
`)}var le=/ {2,}/g;function ae(e){return e.replace(le," ")}function fe(e){return e.replace(y,"")}function ce(e,r={}){if(typeof e!="string")throw new TypeError("revealInvisible expects a string input.");let n=r&&typeof r.format=="function"?r.format:i=>"<U+"+i.toString(16).toUpperCase().padStart(4,"0")+">",u="",o=0;for(let i of A(e,{maxFindings:1/0}).findings)u+=e.slice(o,i.offset)+n(i.codePoint,i.category),o=i.offset+(i.codePoint>65535?2:1);return u+e.slice(o)}var Ee="1.3.11";export{Ee as VERSION,R as getNewlineStats,H as getRecommendedSanctifierOptions,j as inspectText,A as inspectTextDetailed,ce as revealInvisible,L as sanctifyKeyboardOnly,T as sanctifyKeyboardOnlyEmoji,N as sanctifyLoose,D as sanctifyPasted,w as sanctifyStrict,J as summonSanctifier};
|
|
@@ -43,7 +43,10 @@ export function inspectTextDetailed(text: string, options?: InspectDetailedOptio
|
|
|
43
43
|
*/
|
|
44
44
|
export function getNewlineStats(text: string): NewlineStats;
|
|
45
45
|
/**
|
|
46
|
-
* Creates defaultOptions for summonSanctifier based on inspectText result
|
|
46
|
+
* Creates defaultOptions for summonSanctifier based on inspectText result.
|
|
47
|
+
* Recommends only what removes trash without losing meaning: non-ASCII text and emoji are
|
|
48
|
+
* content, so `purgeEmojis` and `keyboardOnlyFilter` stay opt-in, and look-alike spaces
|
|
49
|
+
* become ordinary spaces so that words do not run together.
|
|
47
50
|
* @param {!UnicodeTrashReport} report
|
|
48
51
|
* @return {!SanctifyOptions}
|
|
49
52
|
*/
|
|
@@ -68,7 +68,7 @@ export function sanctifyPasted(text: string): string;
|
|
|
68
68
|
* @param {number} [maxLength=Infinity] - Hard input length cap (UTF-16 code units via `text.length`).
|
|
69
69
|
* @param {'throw'|'truncate'|'noop'} [onMaxLength='throw'] - Behavior when `text.length` exceeds `maxLength`:
|
|
70
70
|
* - `'throw'`: throw a `RangeError`
|
|
71
|
-
* - `'truncate'`: slice to `maxLength` before processing
|
|
71
|
+
* - `'truncate'`: slice to at most `maxLength` before processing, never through a surrogate pair
|
|
72
72
|
* - `'noop'`: return the original input unchanged (skips processing)
|
|
73
73
|
* @param {boolean} [normalizeSpaces=false] - Replace look-alike spaces with an ordinary space. Runs first,
|
|
74
74
|
* so `purgeInvisibleChars` no longer joins the words around them.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The installed version of text-sanctifier, as in its package.json. */
|
|
2
|
-
export const VERSION: "1.3.
|
|
2
|
+
export const VERSION: "1.3.11";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "text-sanctifier",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Find and remove invisible Unicode in web input, model-bound text and repositories.",
|
|
6
6
|
"engines": {
|
|
@@ -48,15 +48,17 @@
|
|
|
48
48
|
"build": "node scripts/build.js",
|
|
49
49
|
"build:types": "tsc -p tsconfig.types.json",
|
|
50
50
|
"test:types": "tsc -p tests/types/tsconfig.json",
|
|
51
|
-
"test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node --expose-gc tests/hiddenText.test.js && node --expose-gc tests/context.test.js && node tests/cli.test.js && node tests/release.test.js",
|
|
51
|
+
"test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node --expose-gc tests/hiddenText.test.js && node --expose-gc tests/context.test.js && node tests/cli.test.js && node tests/release.test.js && node tests/names.test.js && node tests/docs.test.js && node tests/repo.test.js",
|
|
52
52
|
"test-min": "node tests/sanctifyText.test.min.js",
|
|
53
53
|
"check:self": "node bin/text-sanctifier.js check .",
|
|
54
54
|
"check:names": "node scripts/check-names.js",
|
|
55
|
-
"
|
|
55
|
+
"check:repo": "node scripts/check-repo.js",
|
|
56
|
+
"ci": "npm run gen:check && npm run check:names && npm run check:repo && npm run check:self && npm run test && npm run build && npm run size && npm run build:types && npm run test:types && npm run test-min",
|
|
56
57
|
"size": "node scripts/check-size.js",
|
|
57
58
|
"bench": "node --expose-gc scripts/bench.js",
|
|
58
59
|
"release:prep": "node scripts/release.js prep",
|
|
59
60
|
"release:verify": "node scripts/release.js verify",
|
|
61
|
+
"prepublishOnly": "node scripts/release.js check-publish",
|
|
60
62
|
"hooks:install": "git config core.hooksPath scripts/hooks"
|
|
61
63
|
},
|
|
62
64
|
"keywords": [
|
|
@@ -83,7 +85,7 @@
|
|
|
83
85
|
"test": "tests"
|
|
84
86
|
},
|
|
85
87
|
"devDependencies": {
|
|
86
|
-
"esbuild": "
|
|
87
|
-
"typescript": "
|
|
88
|
+
"esbuild": "0.25.12",
|
|
89
|
+
"typescript": "5.9.3"
|
|
88
90
|
}
|
|
89
91
|
}
|
package/src/inspectText.js
CHANGED
|
@@ -322,16 +322,20 @@ export function getNewlineStats(text) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
/**
|
|
325
|
-
* Creates defaultOptions for summonSanctifier based on inspectText result
|
|
325
|
+
* Creates defaultOptions for summonSanctifier based on inspectText result.
|
|
326
|
+
* Recommends only what removes trash without losing meaning: non-ASCII text and emoji are
|
|
327
|
+
* content, so `purgeEmojis` and `keyboardOnlyFilter` stay opt-in, and look-alike spaces
|
|
328
|
+
* become ordinary spaces so that words do not run together.
|
|
326
329
|
* @param {!UnicodeTrashReport} report
|
|
327
330
|
* @return {!SanctifyOptions}
|
|
328
331
|
*/
|
|
329
332
|
export function getRecommendedSanctifierOptions(report) {
|
|
330
333
|
return {
|
|
334
|
+
normalizeSpaces: report.hasInvisibleChars,
|
|
331
335
|
purgeInvisibleChars: report.hasInvisibleChars,
|
|
332
|
-
purgeEmojis:
|
|
336
|
+
purgeEmojis: false,
|
|
333
337
|
nukeControls: report.hasControlChars,
|
|
334
|
-
keyboardOnlyFilter:
|
|
338
|
+
keyboardOnlyFilter: false,
|
|
335
339
|
normalizeNewlines:
|
|
336
340
|
report.hasMixedNewlines || report.newlineStyle === 'CRLF' || report.newlineStyle === 'CR',
|
|
337
341
|
};
|
package/src/sanctifyText.js
CHANGED
|
@@ -294,7 +294,7 @@ export const summonSanctifier = /* @__PURE__ */ Object.assign(createSanctifier,
|
|
|
294
294
|
* @param {number} [maxLength=Infinity] - Hard input length cap (UTF-16 code units via `text.length`).
|
|
295
295
|
* @param {'throw'|'truncate'|'noop'} [onMaxLength='throw'] - Behavior when `text.length` exceeds `maxLength`:
|
|
296
296
|
* - `'throw'`: throw a `RangeError`
|
|
297
|
-
* - `'truncate'`: slice to `maxLength` before processing
|
|
297
|
+
* - `'truncate'`: slice to at most `maxLength` before processing, never through a surrogate pair
|
|
298
298
|
* - `'noop'`: return the original input unchanged (skips processing)
|
|
299
299
|
* @param {boolean} [normalizeSpaces=false] - Replace look-alike spaces with an ordinary space. Runs first,
|
|
300
300
|
* so `purgeInvisibleChars` no longer joins the words around them.
|
|
@@ -324,26 +324,27 @@ export function sanctifyText(
|
|
|
324
324
|
throw new TypeError('sanctifyText expects a string input.');
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
if (onMaxLength !== 'truncate' && onMaxLength !== 'noop' && onMaxLength !== 'throw') {
|
|
328
|
-
onMaxLength = 'throw';
|
|
329
|
-
}
|
|
330
|
-
|
|
331
327
|
if (text.length > maxLength) {
|
|
332
328
|
switch (onMaxLength) {
|
|
333
329
|
case 'truncate':
|
|
334
|
-
|
|
330
|
+
{
|
|
331
|
+
// Cut before a surrogate pair, not through it
|
|
332
|
+
const last = text.charCodeAt(maxLength - 1);
|
|
333
|
+
text = text.slice(0, last >= 0xD800 && last <= 0xDBFF ? maxLength - 1 : maxLength);
|
|
334
|
+
}
|
|
335
335
|
break;
|
|
336
336
|
case 'noop':
|
|
337
337
|
return text;
|
|
338
|
-
|
|
339
|
-
default:
|
|
338
|
+
default: // 'throw', and any invalid value
|
|
340
339
|
throw new RangeError(
|
|
341
340
|
`sanctifyText input length ${text.length} exceeds maxLength ${maxLength}.`
|
|
342
341
|
);
|
|
343
342
|
}
|
|
344
343
|
}
|
|
345
344
|
|
|
346
|
-
|
|
345
|
+
// Unpaired surrogates become U+FFFD first, so removing a character between two
|
|
346
|
+
// halves cannot join them into a new character, such as a hidden tag.
|
|
347
|
+
let cleaned = text.replace(LONE_SURROGATES_REGEX, '\uFFFD');
|
|
347
348
|
|
|
348
349
|
// Look-alike spaces become ordinary spaces, before anything can delete them
|
|
349
350
|
if (normalizeSpaces) cleaned = cleaned.replace(SPACE_LOOKALIKES_REGEX, ' ');
|
|
@@ -389,6 +390,9 @@ export function sanctifyText(
|
|
|
389
390
|
*/
|
|
390
391
|
export const SPACE_LOOKALIKES_REGEX = /[\u00A0\u2000-\u200A\u202F\u205F]/g;
|
|
391
392
|
|
|
393
|
+
/** With the `u` flag a surrogate pair is one character, so this matches only unpaired halves. */
|
|
394
|
+
const LONE_SURROGATES_REGEX = /[\uD800-\uDFFF]/gu;
|
|
395
|
+
|
|
392
396
|
/** Zero-width space, word joiner and BOM: invisible and never needed in plain text. */
|
|
393
397
|
export const ZERO_WIDTH_SPACES_REGEX = /[\u200B\u2060\uFEFF]/g;
|
|
394
398
|
|
|
@@ -497,9 +501,11 @@ function normalizeNewlineChars(text, normalized = '\n') {
|
|
|
497
501
|
* @param {string} text
|
|
498
502
|
* @returns {string}
|
|
499
503
|
*/
|
|
500
|
-
|
|
504
|
+
// The second branch consumes a run with no newline after it, so matching resumes past the
|
|
505
|
+
// run instead of retrying from each of its characters, which took quadratic time.
|
|
506
|
+
const TRIM_SPACES_AROUND_NEWLINES_REGEX = /[ \t]*(\n+)[ \t]*|([ \t]+)/g;
|
|
501
507
|
function trimSpacesAroundNewlineChars(text) {
|
|
502
|
-
return text.replace(TRIM_SPACES_AROUND_NEWLINES_REGEX, '$1');
|
|
508
|
+
return text.replace(TRIM_SPACES_AROUND_NEWLINES_REGEX, '$1$2');
|
|
503
509
|
}
|
|
504
510
|
|
|
505
511
|
|
package/src/version.generated.js
CHANGED
package/src/index.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { inspectText } from './inspectText.js';
|
|
2
|
-
import { inspectTextDetailed } from './inspectText.js';
|
|
3
|
-
import { getNewlineStats } from './inspectText.js';
|
|
4
|
-
import { getRecommendedSanctifierOptions } from './inspectText.js';
|
|
5
|
-
import { summonSanctifier } from './sanctifyText.js';
|
|
6
|
-
import { sanctifyStrict } from './sanctifyText.js';
|
|
7
|
-
import { sanctifyLoose } from './sanctifyText.js';
|
|
8
|
-
import { sanctifyKeyboardOnlyEmoji } from './sanctifyText.js';
|
|
9
|
-
import { sanctifyKeyboardOnly } from './sanctifyText.js';
|
|
10
|
-
import { sanctifyPasted } from './sanctifyText.js';
|
|
11
|
-
import { revealInvisible } from './reveal.js';
|
|
12
|
-
import { VERSION } from './version.generated.js';
|
|
13
|
-
export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier, sanctifyStrict, sanctifyLoose, sanctifyKeyboardOnlyEmoji, sanctifyKeyboardOnly, sanctifyPasted, revealInvisible, VERSION };
|
|
14
|
-
export type UnicodeTrashReport = import('./inspectText.js').UnicodeTrashReport;
|
|
15
|
-
export type TrashCategory = import('./inspectText.js').TrashCategory;
|
|
16
|
-
export type TrashFinding = import('./inspectText.js').TrashFinding;
|
|
17
|
-
export type TrashCounts = import('./inspectText.js').TrashCounts;
|
|
18
|
-
export type DetailedTrashReport = import('./inspectText.js').DetailedTrashReport;
|
|
19
|
-
export type InspectDetailedOptions = import('./inspectText.js').InspectDetailedOptions;
|
|
20
|
-
export type NewlineStats = import('./inspectText.js').NewlineStats;
|
|
21
|
-
export type SanctifyOptions = import('./sanctifyText.js').SanctifyOptions;
|
|
22
|
-
export type RevealOptions = import('./reveal.js').RevealOptions;
|