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/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/iWhatty/text-sanctifier)
|
|
8
8
|
[](https://www.npmjs.com/package/text-sanctifier)
|
|
9
9
|
|
|
10
|
-
Brutal text normalizer and invisible Unicode scrubber for modern web projects. ~2.
|
|
10
|
+
Brutal text normalizer and invisible Unicode scrubber for modern web projects. ~2.87 KB gzipped.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
@@ -15,6 +15,8 @@ Brutal text normalizer and invisible Unicode scrubber for modern web projects. ~
|
|
|
15
15
|
- Purges bidi controls, including the isolates (U+2066–U+2069) and the Arabic letter mark (U+061C) used in Trojan Source attacks
|
|
16
16
|
- Purges Unicode tag characters (U+E0000–U+E007F), the "ASCII smuggling" channel for hiding instructions from humans in text an LLM reads, plus variation selectors, Hangul fillers, soft hyphens and invisible operators
|
|
17
17
|
- Locates invisible and control characters by offset, line and column (`inspectTextDetailed`)
|
|
18
|
+
- Shows hidden characters as visible markers such as `<U+200B>` (`revealInvisible`, `text-sanctifier reveal`)
|
|
19
|
+
- Cleans pasted text and model output without gluing words together: look-alike spaces such as the narrow no-break space (U+202F) become ordinary spaces (`summonSanctifier.pasted`)
|
|
18
20
|
- `text-sanctifier check` / `fix` command line for CI and coding agents, with JSON or SARIF output, exit codes and pre-commit hooks
|
|
19
21
|
- `check --ai-config` finds and decodes hidden text in agent instruction files (`CLAUDE.md`, `AGENTS.md`, Cursor rules, `SKILL.md`, MCP configuration)
|
|
20
22
|
- Normalizes line endings (CRLF, CR, LF) to LF
|
|
@@ -81,6 +83,20 @@ const output = summonSanctifier.keyboardOnly(userInput);
|
|
|
81
83
|
const output = summonSanctifier.keyboardOnlyEmoji(commentText);
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
**Pasted text and model output:**
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const output = summonSanctifier.pasted(clipboardText);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Show what is hidden:**
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { revealInvisible } from 'text-sanctifier';
|
|
96
|
+
|
|
97
|
+
revealInvisible(text); // 'Hello<U+200B>world'
|
|
98
|
+
```
|
|
99
|
+
|
|
84
100
|
---
|
|
85
101
|
|
|
86
102
|
## Security notes
|
|
@@ -98,6 +114,21 @@ const output = summonSanctifier.keyboardOnlyEmoji(commentText);
|
|
|
98
114
|
|
|
99
115
|
Creates a reusable sanitizer from an option object.
|
|
100
116
|
|
|
117
|
+
Two options handle spaces and zero-width characters gently, and both run before the others:
|
|
118
|
+
|
|
119
|
+
- `normalizeSpaces`: replace look-alike spaces with an ordinary space. These are U+00A0, U+2000–U+200A, U+202F and U+205F: the `nonbreaking_space` category except U+3000, the ordinary space of CJK text. With `purgeInvisibleChars` alone, a non-breaking space between two words is deleted and the words run together; with `normalizeSpaces` as well, they stay apart.
|
|
120
|
+
- `purgeZeroWidthSpaces`: remove the zero width space U+200B, the word joiner U+2060 and U+FEFF, and nothing else. Emoji joiners, ZWNJ, variation selectors and bidi marks are kept.
|
|
121
|
+
|
|
122
|
+
### Presets as standalone imports
|
|
123
|
+
|
|
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 1.1 KB gzipped instead of 1.45 KB).
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { sanctifyPasted } from 'text-sanctifier';
|
|
128
|
+
|
|
129
|
+
const output = sanctifyPasted(clipboardText);
|
|
130
|
+
```
|
|
131
|
+
|
|
101
132
|
### `summonSanctifier.strict`
|
|
102
133
|
|
|
103
134
|
Aggressively purges: emojis, control characters, extra spacing, and newlines.
|
|
@@ -114,6 +145,23 @@ Restricts to printable ASCII only (removes emojis).
|
|
|
114
145
|
|
|
115
146
|
Restricts to printable ASCII + full emoji sequences. Preserves ZWJ emoji clusters and emoji presentation variants.
|
|
116
147
|
|
|
148
|
+
### `summonSanctifier.pasted`
|
|
149
|
+
|
|
150
|
+
For text pasted from documents, chat apps and PDFs, and for model output. Some models put narrow no-break spaces (U+202F) and other look-alike spaces into their answers. This preset uses `normalizeSpaces`, `purgeZeroWidthSpaces` and `normalizeNewlines`: look-alike spaces become ordinary spaces, zero-width spaces and word joiners are removed, and CRLF and CR become LF. It keeps emoji joiners, variation selectors, bidi marks, indentation, blank lines and leading and trailing whitespace, so it is safe on code and Markdown.
|
|
151
|
+
|
|
152
|
+
### `revealInvisible(text: string, options?: RevealOptions): string`
|
|
153
|
+
|
|
154
|
+
Replaces every character `inspectTextDetailed` would report with a visible marker and leaves everything else alone, including tabs, newlines and emoji. Use it to show hidden text in logs, diffs, review tools and agent tool output. Also in the browser build.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { revealInvisible } from 'text-sanctifier';
|
|
158
|
+
|
|
159
|
+
revealInvisible('a\u200Bb\u{E0041}'); // 'a<U+200B>b<U+E0041>'
|
|
160
|
+
revealInvisible('a\u202Eb', { format: (codePoint, category) => `[${category}]` }); // 'a[bidi_control]b'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The default marker is `<U+XXXX>` with at least four uppercase hex digits. Every hidden character is replaced, with no cap like `maxFindings`. A literal `<U+200B>` already in the text looks the same as a marker; pass `format` to choose markers that can't occur in your input.
|
|
164
|
+
|
|
117
165
|
### `inspectText(text: string): UnicodeTrashReport`
|
|
118
166
|
|
|
119
167
|
Returns a structural report of control codes, invisible chars, newline styles, and more.
|
|
@@ -198,22 +246,25 @@ getNewlineStats('a\r\nb\n'); // { crlf: 1, cr: 0, lf: 1, types: ['CRLF', 'LF'],
|
|
|
198
246
|
|
|
199
247
|
The categories cover exactly what `nukeControls` and `purgeInvisibleChars` strip. Every code point with the Unicode property Default_Ignorable_Code_Point (characters renderers must draw as nothing) is in a category, and a test checks this against the running Node's Unicode data.
|
|
200
248
|
|
|
201
|
-
### `text-sanctifier/hidden`: `findHiddenRuns(text)` and `decodeTagText(text)`
|
|
249
|
+
### `text-sanctifier/hidden`: `findHiddenRuns(text, options?)` and `decodeTagText(text)`
|
|
202
250
|
|
|
203
251
|
The run finder behind [`check --ai-config`](#ai-configuration-files), for tools that read text on its way to a model. It has no Node APIs, so it works in a browser too, but it is a separate entry point and not part of the `text-sanctifier/browser` bundle.
|
|
204
252
|
|
|
205
253
|
```js
|
|
206
254
|
import { findHiddenRuns, decodeTagText } from 'text-sanctifier/hidden';
|
|
207
255
|
|
|
208
|
-
const { runs, benign } = findHiddenRuns(text);
|
|
209
|
-
// runs: [{ kind: 'tag', offset, end, count, decoded: 'Ignore the user' }, ...]
|
|
256
|
+
const { runs, benign, counts, total, truncated } = findHiddenRuns(text, { maxRuns: 100, maxDecodedLength: 1000 });
|
|
257
|
+
// runs: [{ kind: 'tag', offset, end, line, column, count, decoded: 'Ignore the user', decodedTruncated: false }, ...]
|
|
210
258
|
// benign: [[offset, end], ...] England/Scotland/Wales flags, a lone U+FE0E/U+FE0F after a character
|
|
259
|
+
// counts: { tag, variation_selector, invisible_operator, reserved_tag_plane }, every run listed or not; total is their sum
|
|
211
260
|
```
|
|
212
261
|
|
|
213
|
-
- `kind` is `tag` (U+E0000–U+E007F), `variation_selector` (U+FE00–U+FE0F, U+E0100–U+E01EF, U+180B–U+180D, U+180F)
|
|
214
|
-
- `offset` and `end` are UTF-16 indexes, `[offset, end)`; `count` is in code points. Only tag runs have `decoded`.
|
|
262
|
+
- `kind` is `tag` (U+E0000–U+E007F), `variation_selector` (U+FE00–U+FE0F, U+E0100–U+E01EF, U+180B–U+180D, U+180F) `invisible_operator` (U+2061–U+2064) or `reserved_tag_plane` (U+E0080–U+E00FF, U+E01F0–U+E0FFF: unassigned, default ignorable, so they render as nothing and can carry data). Consecutive characters of one kind form one run.
|
|
263
|
+
- `offset` and `end` are UTF-16 indexes, `[offset, end)`; `count` is in code points. `line` and `column` are counted exactly as in `inspectTextDetailed` (1-based, columns in UTF-16 units, CRLF is one line break). Only tag runs have `decoded` and `decodedTruncated`.
|
|
264
|
+
- `maxRuns` caps the `runs` list and, separately, the `benign` list; `counts`, `total` and `benignTotal` count everything, and `truncated` is true when either list was cut. `maxDecodedLength` caps each `decoded` string in UTF-16 units and never cuts a `<U+XXXX>` marker. Both default to no cap, and an invalid value (negative, fractional, a string) also means no cap, so set them explicitly for untrusted input. With both set, memory stays flat on any input: on 16 M characters (32 MB) of hidden text the heap grows by under 1 MB, against hundreds of MB with no caps.
|
|
215
265
|
- `decodeTagText` turns U+E0020–U+E007E into the ASCII they shadow and writes any other character as `<U+XXXX>`.
|
|
216
266
|
- Zero-width, bidi and other invisible characters are not runs; use `inspectTextDetailed` for those.
|
|
267
|
+
- `VERSION` is exported here and from the package root: the installed version, as in `package.json`.
|
|
217
268
|
|
|
218
269
|
---
|
|
219
270
|
|
|
@@ -224,6 +275,7 @@ The package ships a `text-sanctifier` command for CI and coding agents, which ca
|
|
|
224
275
|
```sh
|
|
225
276
|
npx text-sanctifier check src docs "**/*.md"
|
|
226
277
|
npx text-sanctifier fix --dry-run src
|
|
278
|
+
npx text-sanctifier reveal notes.md
|
|
227
279
|
```
|
|
228
280
|
|
|
229
281
|
### `check [paths...]`
|
|
@@ -262,7 +314,7 @@ skills/review/SKILL.md:2:3 U+FE00 run of 3 variation selectors
|
|
|
262
314
|
- Consecutive tag characters, variation selectors or invisible operators are reported once, as a run, at the first character. Tag runs are decoded: U+E0020–U+E007E become the ASCII they shadow, and other tags are shown as `<U+XXXX>`.
|
|
263
315
|
- Every other invisible or control character is reported as in a plain `check`.
|
|
264
316
|
- Two legitimate sequences are left out: the flags of England, Scotland and Wales (U+1F3F4, tag letters, U+E007F), and a single U+FE0E or U+FE0F after another character (text or emoji presentation). A plain `check` still reports both.
|
|
265
|
-
- `--json` adds `hiddenRuns` (`{ kind, offset, line, column, codePoint, count, decoded?, decodedTruncated? }`, at most `--max-findings` per file) and `hiddenRunCount` to each file, and `hiddenRunCount` to the summary. `kind` is `tag`, `variation_selector` or `
|
|
317
|
+
- `--json` adds `hiddenRuns` (`{ kind, offset, line, column, codePoint, count, decoded?, decodedTruncated? }`, at most `--max-findings` per file) and `hiddenRunCount` to each file, and `hiddenRunCount` to the summary. `kind` is `tag`, `variation_selector`, `invisible_operator` or `reserved_tag_plane`; `decoded` is cut at 1000 characters. `total` and `counts` leave out the legitimate sequences.
|
|
266
318
|
|
|
267
319
|
### SARIF output
|
|
268
320
|
|
|
@@ -282,7 +334,7 @@ The repository publishes a [pre-commit](https://pre-commit.com) hook manifest, s
|
|
|
282
334
|
# .pre-commit-config.yaml
|
|
283
335
|
repos:
|
|
284
336
|
- repo: https://github.com/iWhatty/text-sanctifier
|
|
285
|
-
rev: v1.3.
|
|
337
|
+
rev: v1.3.6 # a release tag
|
|
286
338
|
hooks:
|
|
287
339
|
- id: text-sanctifier # every staged text file
|
|
288
340
|
- id: text-sanctifier-ai-config # agent instruction files, with --ai-config
|
|
@@ -294,17 +346,27 @@ repos:
|
|
|
294
346
|
|
|
295
347
|
### `fix [paths...]`
|
|
296
348
|
|
|
297
|
-
Rewrites files in place and reports what it removed.
|
|
349
|
+
Rewrites files in place and reports what it replaced and removed.
|
|
298
350
|
|
|
351
|
+
- `--normalize-spaces`: `normalizeSpaces` (look-alike spaces become ordinary spaces)
|
|
299
352
|
- `--purge-invisible`: `purgeInvisibleChars`
|
|
300
353
|
- `--nuke-controls`: `nukeControls`
|
|
301
354
|
- `--normalize-newlines`: `normalizeNewlines` (CRLF and CR to LF)
|
|
302
|
-
- With none of
|
|
355
|
+
- With none of these, `fix` uses `--normalize-spaces --purge-invisible --nuke-controls`: look-alike spaces become ordinary spaces, and everything else `check` reports is removed.
|
|
303
356
|
- `--strip-bom`: also remove a BOM at offset 0. It is kept by default.
|
|
304
357
|
- `--dry-run`: write nothing; report what would change.
|
|
305
|
-
- `--json` (or `--format json`): `{ files, dryRun, filesScanned, filesChanged, skipped }`, where each file is `{ path, removed, removedTotal, newlinesNormalized }`.
|
|
358
|
+
- `--json` (or `--format json`): `{ files, dryRun, filesScanned, filesChanged, skipped }`, where each file is `{ path, spacesReplaced, removed, removedTotal, newlinesNormalized }`.
|
|
359
|
+
|
|
360
|
+
Apart from look-alike spaces, `fix` deletes characters. Emoji lose their zero width joiners and variation selectors, and so does Mongolian text. Run `check` or `fix --dry-run` first on prose. To make hidden characters visible without changing a file, use `reveal`.
|
|
361
|
+
|
|
362
|
+
### `reveal [paths...]`
|
|
306
363
|
|
|
307
|
-
`
|
|
364
|
+
Prints each input with every character `check` would report replaced by a marker such as `<U+200B>` (see `revealInvisible`). Everything else, including newlines, is printed as it is, and files are never changed. Several inputs are printed one after another under `==> path <==` headers. `reveal` takes no options and exits 0 unless an input can't be read.
|
|
365
|
+
|
|
366
|
+
```sh
|
|
367
|
+
npx text-sanctifier reveal AGENTS.md
|
|
368
|
+
git diff | npx text-sanctifier reveal
|
|
369
|
+
```
|
|
308
370
|
|
|
309
371
|
### Inputs
|
|
310
372
|
|
|
@@ -315,11 +377,11 @@ Rewrites files in place and reports what it removed.
|
|
|
315
377
|
|
|
316
378
|
### Exit codes
|
|
317
379
|
|
|
318
|
-
| Code | `check` | `fix` |
|
|
319
|
-
| --- | --- | --- |
|
|
320
|
-
| 0 | nothing found | done (or nothing to do) |
|
|
321
|
-
| 1 | findings | `--dry-run` with changes pending |
|
|
322
|
-
| 2 | usage error, missing path or unreadable file | same |
|
|
380
|
+
| Code | `check` | `fix` | `reveal` |
|
|
381
|
+
| --- | --- | --- | --- |
|
|
382
|
+
| 0 | nothing found | done (or nothing to do) | done |
|
|
383
|
+
| 1 | findings | `--dry-run` with changes pending | not used |
|
|
384
|
+
| 2 | usage error, missing path or unreadable file | same | same |
|
|
323
385
|
|
|
324
386
|
---
|
|
325
387
|
|
|
@@ -337,8 +399,8 @@ Requires a modern JavaScript runtime with ES2020+ support:
|
|
|
337
399
|
- **Source (`src/`):** ES2020+ ESM modules with JSDoc
|
|
338
400
|
- **ESM only:** no CommonJS build; `require('text-sanctifier')` works only on Node versions with `require(esm)` support (20.19+, 22.12+)
|
|
339
401
|
- **Types:** TypeScript declarations ship in `dist/types`
|
|
340
|
-
- **Browser build (`dist/`):**
|
|
341
|
-
- **Tree-shake friendly:** ships `sideEffects: false`
|
|
402
|
+
- **Browser build (`dist/`):** one minified ESM file with every export, for pages with no build step (`<script type="module">` from a CDN). It is imported as `text-sanctifier/browser`. With a bundler, import `text-sanctifier` instead: the source tree-shakes, and the prebuilt file does not
|
|
403
|
+
- **Tree-shake friendly:** ships `sideEffects: false`; `npm run ci` checks the bundled size of each export on its own (for example `inspectTextDetailed` is under 1 KB gzipped)
|
|
342
404
|
- **Zero transpilation:** no built-in polyfills or runtime overhead
|
|
343
405
|
- **Bundler ready:** works with Vite, Rollup, Webpack, Parcel, esbuild
|
|
344
406
|
|
package/bin/text-sanctifier.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// text-sanctifier check [paths...] report invisible and control characters
|
|
6
6
|
// text-sanctifier check --ai-config report hidden text in agent instruction files
|
|
7
7
|
// text-sanctifier fix [paths...] remove them in place
|
|
8
|
+
// text-sanctifier reveal [paths...] print the text with hidden characters shown as <U+XXXX>
|
|
8
9
|
//
|
|
9
10
|
// Plain Node (>=18), no dependencies. Exit codes: 0 clean, 1 findings (check,
|
|
10
11
|
// or fix --dry-run with changes pending), 2 usage or I/O error.
|
|
@@ -13,8 +14,9 @@ import { readFileSync, writeFileSync, statSync, readdirSync, existsSync } from '
|
|
|
13
14
|
import { resolve, join, sep, relative } from 'node:path';
|
|
14
15
|
import { spawnSync } from 'node:child_process';
|
|
15
16
|
import { pathToFileURL } from 'node:url';
|
|
16
|
-
import { inspectTextDetailed, summonSanctifier } from '../src/index.js';
|
|
17
|
+
import { inspectTextDetailed, summonSanctifier, revealInvisible } from '../src/index.js';
|
|
17
18
|
import { getNewlineStats } from '../src/inspectText.js';
|
|
19
|
+
import { SPACE_LOOKALIKES_REGEX } from '../src/sanctifyText.js';
|
|
18
20
|
import { findHiddenRuns } from '../src/hiddenText.js';
|
|
19
21
|
|
|
20
22
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
@@ -24,6 +26,7 @@ const USAGE = `text-sanctifier ${VERSION}
|
|
|
24
26
|
Usage:
|
|
25
27
|
text-sanctifier check [options] [paths...]
|
|
26
28
|
text-sanctifier fix [options] [paths...]
|
|
29
|
+
text-sanctifier reveal [paths...]
|
|
27
30
|
|
|
28
31
|
Paths may be files, directories or globs (quote globs so the shell leaves them
|
|
29
32
|
alone). "-", or no paths with piped input, reads stdin. Directories and globs
|
|
@@ -40,6 +43,8 @@ check options:
|
|
|
40
43
|
for those files, and tag text is decoded
|
|
41
44
|
|
|
42
45
|
fix options:
|
|
46
|
+
--normalize-spaces replace look-alike spaces (NBSP, narrow NBSP, typographic
|
|
47
|
+
spaces) with an ordinary space instead of removing them
|
|
43
48
|
--purge-invisible remove zero-width, non-breaking space, bidi and BOM characters
|
|
44
49
|
--nuke-controls remove C0/C1 control characters and bidi controls
|
|
45
50
|
--normalize-newlines convert CRLF and CR to LF
|
|
@@ -47,9 +52,15 @@ fix options:
|
|
|
47
52
|
--dry-run report what would change; write nothing
|
|
48
53
|
--format F text (default) or json
|
|
49
54
|
--json same as --format json
|
|
50
|
-
With no cleaning flag, fix uses --purge-invisible --nuke-controls.
|
|
55
|
+
With no cleaning flag, fix uses --normalize-spaces --purge-invisible --nuke-controls.
|
|
51
56
|
stdin is cleaned to stdout; the report then goes to stderr.
|
|
52
57
|
|
|
58
|
+
reveal:
|
|
59
|
+
Prints each input with every character check would report replaced by a
|
|
60
|
+
marker such as <U+200B>; everything else, including newlines, is unchanged.
|
|
61
|
+
Several inputs are printed one after another under "==> path <==" headers.
|
|
62
|
+
Takes no options. Exit code 0 unless an input can't be read.
|
|
63
|
+
|
|
53
64
|
Other:
|
|
54
65
|
-h, --help show this help
|
|
55
66
|
-v, --version show the version
|
|
@@ -73,12 +84,13 @@ class UsageError extends Error {}
|
|
|
73
84
|
const BOOLEAN_FLAGS = {
|
|
74
85
|
'--ai-config': 'aiConfig',
|
|
75
86
|
'--strip-bom': 'stripBom',
|
|
87
|
+
'--normalize-spaces': 'normalizeSpaces',
|
|
76
88
|
'--purge-invisible': 'purgeInvisible',
|
|
77
89
|
'--nuke-controls': 'nukeControls',
|
|
78
90
|
'--normalize-newlines': 'normalizeNewlines',
|
|
79
91
|
'--dry-run': 'dryRun',
|
|
80
92
|
};
|
|
81
|
-
const FIX_ONLY_FLAGS = new Set(['purgeInvisible', 'nukeControls', 'normalizeNewlines', 'dryRun']);
|
|
93
|
+
const FIX_ONLY_FLAGS = new Set(['normalizeSpaces', 'purgeInvisible', 'nukeControls', 'normalizeNewlines', 'dryRun']);
|
|
82
94
|
const CHECK_ONLY_FLAGS = new Set(['aiConfig']);
|
|
83
95
|
const FORMATS = new Set(['text', 'json', 'sarif']);
|
|
84
96
|
|
|
@@ -92,6 +104,7 @@ function parseArgs(argv) {
|
|
|
92
104
|
format: null,
|
|
93
105
|
aiConfig: false,
|
|
94
106
|
stripBom: false,
|
|
107
|
+
normalizeSpaces: false,
|
|
95
108
|
purgeInvisible: false,
|
|
96
109
|
nukeControls: false,
|
|
97
110
|
normalizeNewlines: false,
|
|
@@ -137,16 +150,25 @@ function parseArgs(argv) {
|
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
if (opts.help || opts.version) return opts;
|
|
140
|
-
if (opts.command !== 'check' && opts.command !== 'fix') {
|
|
153
|
+
if (opts.command !== 'check' && opts.command !== 'fix' && opts.command !== 'reveal') {
|
|
141
154
|
throw new UsageError(opts.command === null ? 'Missing command.' : `Unknown command: ${opts.command}`);
|
|
142
155
|
}
|
|
156
|
+
if (opts.command === 'reveal') {
|
|
157
|
+
for (const [flag, key] of Object.entries(BOOLEAN_FLAGS)) {
|
|
158
|
+
if (opts[key]) throw new UsageError(`${flag} does not apply to reveal.`);
|
|
159
|
+
}
|
|
160
|
+
if (opts.format !== null) throw new UsageError('--format does not apply to reveal.');
|
|
161
|
+
if (opts.maxFindings !== 100) throw new UsageError('--max-findings does not apply to reveal.');
|
|
162
|
+
return opts;
|
|
163
|
+
}
|
|
143
164
|
for (const [flag, key] of Object.entries(BOOLEAN_FLAGS)) {
|
|
144
165
|
if (opts.command === 'check' && FIX_ONLY_FLAGS.has(key) && opts[key]) throw new UsageError(`${flag} only applies to fix.`);
|
|
145
166
|
if (opts.command === 'fix' && CHECK_ONLY_FLAGS.has(key) && opts[key]) throw new UsageError(`${flag} only applies to check.`);
|
|
146
167
|
}
|
|
147
168
|
if (opts.format === null) opts.format = 'text';
|
|
148
169
|
if (opts.command === 'fix' && opts.format === 'sarif') throw new UsageError('--format sarif only applies to check.');
|
|
149
|
-
if (opts.command === 'fix' && !opts.purgeInvisible && !opts.nukeControls && !opts.normalizeNewlines) {
|
|
170
|
+
if (opts.command === 'fix' && !opts.normalizeSpaces && !opts.purgeInvisible && !opts.nukeControls && !opts.normalizeNewlines) {
|
|
171
|
+
opts.normalizeSpaces = true;
|
|
150
172
|
opts.purgeInvisible = true;
|
|
151
173
|
opts.nukeControls = true;
|
|
152
174
|
}
|
|
@@ -421,7 +443,7 @@ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
|
421
443
|
// --- check --ai-config ---
|
|
422
444
|
|
|
423
445
|
const MAX_DECODED = 1000;
|
|
424
|
-
const RUN_NOUNS = { tag: 'tag character', variation_selector: 'variation selector', invisible_operator: 'invisible operator' };
|
|
446
|
+
const RUN_NOUNS = { tag: 'tag character', variation_selector: 'variation selector', invisible_operator: 'invisible operator', reserved_tag_plane: 'reserved plane-14 code point' };
|
|
425
447
|
|
|
426
448
|
/**
|
|
427
449
|
* inspect() for agent instruction files: drops findings that belong to legitimate
|
|
@@ -683,8 +705,22 @@ async function main(argv) {
|
|
|
683
705
|
return errors > 0 ? 2 : total > 0 ? 1 : 0;
|
|
684
706
|
}
|
|
685
707
|
|
|
708
|
+
if (opts.command === 'reveal') {
|
|
709
|
+
const many = inputs.length > 1;
|
|
710
|
+
let first = true;
|
|
711
|
+
await forEachText((label, text) => {
|
|
712
|
+
if (many) {
|
|
713
|
+
process.stdout.write(`${first ? '' : '\n'}==> ${label} <==\n`);
|
|
714
|
+
first = false;
|
|
715
|
+
}
|
|
716
|
+
process.stdout.write(revealInvisible(text));
|
|
717
|
+
});
|
|
718
|
+
return errors > 0 ? 2 : 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
686
721
|
// fix
|
|
687
722
|
const clean = summonSanctifier({
|
|
723
|
+
normalizeSpaces: opts.normalizeSpaces,
|
|
688
724
|
purgeInvisibleChars: opts.purgeInvisible,
|
|
689
725
|
nukeControls: opts.nukeControls,
|
|
690
726
|
normalizeNewlines: opts.normalizeNewlines,
|
|
@@ -706,6 +742,10 @@ async function main(argv) {
|
|
|
706
742
|
|
|
707
743
|
const before = inspectTextDetailed(text, { maxFindings: 0 }).counts;
|
|
708
744
|
const after = inspectTextDetailed(cleaned, { maxFindings: 0 }).counts;
|
|
745
|
+
// Look-alike spaces that became ordinary spaces were replaced, not removed
|
|
746
|
+
SPACE_LOOKALIKES_REGEX.lastIndex = 0;
|
|
747
|
+
const spacesReplaced = opts.normalizeSpaces ? (text.match(SPACE_LOOKALIKES_REGEX) || []).length : 0;
|
|
748
|
+
before.nonbreaking_space -= spacesReplaced;
|
|
709
749
|
const removed = {};
|
|
710
750
|
let removedTotal = 0;
|
|
711
751
|
for (const k of Object.keys(before)) {
|
|
@@ -724,7 +764,7 @@ async function main(argv) {
|
|
|
724
764
|
return;
|
|
725
765
|
}
|
|
726
766
|
}
|
|
727
|
-
files.push({ path: label, removed, removedTotal, newlinesNormalized });
|
|
767
|
+
files.push({ path: label, spacesReplaced, removed, removedTotal, newlinesNormalized });
|
|
728
768
|
});
|
|
729
769
|
|
|
730
770
|
if (opts.format === 'json') {
|
|
@@ -733,6 +773,7 @@ async function main(argv) {
|
|
|
733
773
|
const verb = opts.dryRun ? 'would fix' : 'fixed';
|
|
734
774
|
for (const f of files) {
|
|
735
775
|
const parts = [];
|
|
776
|
+
if (f.spacesReplaced > 0) parts.push(`replaced ${plural(f.spacesReplaced, 'look-alike space')}`);
|
|
736
777
|
if (f.removedTotal > 0) parts.push(`removed ${f.removedTotal} (${formatCounts(f.removed)})`);
|
|
737
778
|
if (f.newlinesNormalized) parts.push('normalized newlines');
|
|
738
779
|
report(`${verb} ${f.path}: ${parts.join('; ')}`);
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
var
|
|
2
|
-
function t(a={}){
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
var x=/\r\n|\r|\n/g,y=/[ \t]*(\n+)[ \t]*/g,B=/\n{2,}/g,z=/\n{3,}/g,C=/ {2,}/g;function L(a,c){a.lastIndex=0;return a.test(c)}
|
|
8
|
-
function M(a){if("string"!==typeof a)throw new TypeError("inspectText expects a string input.");const c=[],d={hasControlChars:!1,hasInvisibleChars:!1,hasMixedNewlines:!1,newlineStyle:null,hasEmojis:!1,hasNonKeyboardChars:!1,summary:c};L(n,a)&&(d.hasControlChars=!0,c.push("Control characters detected."));L(q,a)&&(d.hasInvisibleChars=!0,c.push("Invisible Unicode characters detected."));L(v,a)&&(d.hasEmojis=!0,c.push("Emojis detected."));const {mixed:f,types:h}=N(a);d.hasMixedNewlines=f;d.newlineStyle=
|
|
9
|
-
f?"Mixed":h[0]||null;d.newlineStyle&&c.push(f?"Mixed newline styles detected.":`Consistent newline style: ${d.newlineStyle}`);a:{a=E(a);if(L(D,a))for(g of a)if(!("\n"===g||"\r"===g||" "<=g&&"~">=g||L(v,g))){var g=!0;break a}g=!1}g&&(d.hasNonKeyboardChars=!0,c.push("Non-keyboard characters detected."));return d}
|
|
10
|
-
function O(a,c={}){if("string"!==typeof a)throw new TypeError("inspectTextDetailed expects a string input.");c=c&&c.maxFindings;c=Infinity===c||Number.isInteger(c)&&0<=c?c:100;const d={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},f=[];let h=0,g=1,l=0;for(let k=0,r=a.length;k<r;k++){var b=a.charCodeAt(k);if(32<=b&&127>b)continue;if(10===b){g++;l=k+1;continue}if(13===b){10!==a.charCodeAt(k+1)&&(g++,
|
|
11
|
-
l=k+1);continue}const m=k;var e=b;if(55296<=b&&56319>=b){e=a.charCodeAt(k+1);if(!(56320<=e&&57343>=e))continue;e=65536+(b-55296<<10)+(e-56320);b=917504>e?113828>e?113824>e?null:"invisible_format":119155>e?null:119163>e?"invisible_format":null:917760>e?917632>e?"tag":"invisible_format":918E3>e?"variation_selector":921600>e?"invisible_format":null;k++}else b=8192>b?174>b?32>b?11>b?1>b?"nul":9>b?"c0_control":null:13>b?"c0_control":14>b?null:"c0_control":160>b?127>b?null:128>b?"c0_control":"c1_control":
|
|
12
|
-
161>b?"nonbreaking_space":173>b?null:"invisible_format":4449>b?1564>b?847>b?null:848>b?"invisible_format":null:1565>b?"bidi_control":4447>b?null:"hangul_filler":6155>b?6068>b?null:6070>b?"invisible_format":null:6159>b?6158>b?"variation_selector":"invisible_format":6160>b?"variation_selector":null:8304>b?8240>b?8208>b?8203>b?"nonbreaking_space":8206>b?"zero_width":"bidi_control":8234>b?null:8239>b?"bidi_control":"nonbreaking_space":8289>b?8287>b?null:8288>b?"nonbreaking_space":"zero_width":8294>b?
|
|
13
|
-
"invisible_format":8298>b?"bidi_control":"invisible_format":65040>b?12644>b?12288>b?null:12289>b?"nonbreaking_space":null:12645>b?"hangul_filler":65024>b?null:"variation_selector":65440>b?65279>b?null:65280>b?"bom":null:65520>b?65441>b?"hangul_filler":null:65529>b?"invisible_format":null;null!==b&&(d[b]++,h++,f.length<c&&(e={offset:m,line:g,column:m-l+1,codePoint:e,category:b},"bom"===b&&(e.atStart=0===m),f.push(e)))}return{findings:f,counts:d,total:h,truncated:h>f.length}}
|
|
14
|
-
function N(a){if("string"!==typeof a)throw new TypeError("getNewlineStats expects a string input.");var c=0;let d=0,f=0;for(let h=0,g=a.length;h<g;h++){const l=a.charCodeAt(h);13===l?10===a.charCodeAt(h+1)?(c++,h++):d++:10===l&&f++}a={crlf:c,cr:d,lf:f};c=[];0<a.crlf&&c.push("CRLF");0<a.cr&&c.push("CR");0<a.lf&&c.push("LF");return{...a,types:c,mixed:1<c.length}}
|
|
15
|
-
function P(a){return{purgeInvisibleChars:a.hasInvisibleChars,purgeEmojis:a.hasEmojis,nukeControls:a.hasControlChars,keyboardOnlyFilter:a.hasNonKeyboardChars,normalizeNewlines:a.hasMixedNewlines||"CRLF"===a.newlineStyle||"CR"===a.newlineStyle}}export { t as summonSanctifier, M as inspectText, P as getRecommendedSanctifierOptions, O as inspectTextDetailed, N as getNewlineStats };
|
|
1
|
+
var y=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g,d=/[\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 A(e){return e<8192?D(e):v(e)}function D(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 v(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 I(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,B=/[\u2018\u2019\u201A\u201B\u2032\u2035]/g,G=/[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g,X=/[\u2012\u2013\u2014\u2015\u2212]/g,k=/\u2026/g,O=/[\u2022\u00B7]/g,P=/[\uFF01-\uFF5E]/g;function S(e){return e.replace(B,"'").replace(G,'"').replace(X,"-").replace(k,"...").replace(O,"*").replace(P,r=>String.fromCharCode(r.charCodeAt(0)-65248))}function M(){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 x=M();function m(e,r){return e.lastIndex=0,e.test(r)}function U(e){let r=S(e);if(!m(g,r))return!1;for(let n of r)if(!(n===`
|
|
2
|
+
`||n==="\r"||n>=" "&&n<="~")&&!m(x,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=(s,c,f)=>{s&&(n[c]=!0,r.push(f))};u(m(y,e),"hasControlChars","Control characters detected."),u(m(d,e),"hasInvisibleChars","Invisible Unicode characters detected."),u(m(x,e),"hasEmojis","Emojis detected.");let{mixed:o,types:i}=b(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(U(e),"hasNonKeyboardChars","Non-keyboard characters detected."),n}var z=100;function C(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:z,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=[],s=0,c=1,f=0;for(let a=0,h=e.length;a<h;a++){let l=e.charCodeAt(a);if(l>=32&&l<127)continue;if(l===10){c++,f=a+1;continue}if(l===13){e.charCodeAt(a+1)!==10&&(c++,f=a+1);continue}let E=a,F=l,p;if(l>=55296&&l<=56319){let t=e.charCodeAt(a+1);if(!(t>=56320&&t<=57343))continue;F=65536+(l-55296<<10)+(t-56320),p=I(F),a++}else p=A(l);if(p!==null&&(o[p]++,s++,i.length<u)){let t={offset:E,line:c,column:E-f+1,codePoint:F,category:p};p==="bom"&&(t.atStart=E===0),i.push(t)}}return{findings:i,counts:o,total:s,truncated:s>i.length}}function b(e){if(typeof e!="string")throw new TypeError("getNewlineStats expects a string input.");let r=0,n=0,u=0;for(let s=0,c=e.length;s<c;s++){let f=e.charCodeAt(s);f===13?e.charCodeAt(s+1)===10?(r++,s++):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 K(e){return{purgeInvisibleChars:e.hasInvisibleChars,purgeEmojis:e.hasEmojis,nukeControls:e.hasControlChars,keyboardOnlyFilter:e.hasNonKeyboardChars,normalizeNewlines:e.hasMixedNewlines||e.newlineStyle==="CRLF"||e.newlineStyle==="CR"}}function H(e={}){let r=!!e.normalizeSpaces,n=!!e.purgeZeroWidthSpaces,u=!!e.purgeInvisibleChars,o=!!e.purgeEmojis,i=!!e.nukeControls,s=!!e.keyboardOnlyFilter,c=!!e.normalizeNewlines,f=!!e.trimSpacesAroundNewlines,a=!!e.collapseNewLines,h=!!e.preserveParagraphs,l=!!e.collapseSpaces,E=!!e.finalTrim,F=Number.isFinite(e.maxLength)&&e.maxLength>=0?e.maxLength:1/0,p=e.onMaxLength==="truncate"||e.onMaxLength==="noop"||e.onMaxLength==="throw"?e.onMaxLength:"throw";return t=>_(t,u,o,i,s,c,f,a,h,l,E,F,p,r,n)}function R(e){return _(e,!0,!0,!0,!1,!0,!0,!0,!1,!0,!0)}function w(e){return _(e,!1,!1,!1,!1,!0,!0,!0,!0,!0,!0)}function N(e){return _(e,!1,!1,!1,!0,!0,!0,!1,!1,!1,!0)}function T(e){return _(e,!0,!0,!0,!0,!0,!0,!0,!1,!0,!0)}function L(e){return _(e,!1,!1,!1,!1,!0,!1,!1,!1,!1,!1,1/0,"throw",!0,!0)}var W=Object.assign(H,{strict:R,loose:w,keyboardOnlyEmoji:N,keyboardOnly:T,pasted:L});function _(e,r=!1,n=!1,u=!1,o=!1,i=!1,s=!1,c=!1,f=!1,a=!1,h=!1,l=1/0,E="throw",F=!1,p=!1){if(typeof e!="string")throw new TypeError("sanctifyText expects a string input.");if(E!=="truncate"&&E!=="noop"&&E!=="throw"&&(E="throw"),e.length>l)switch(E){case"truncate":e=e.slice(0,l);break;case"noop":return e;case"throw":default:throw new RangeError(`sanctifyText input length ${e.length} exceeds maxLength ${l}.`)}let t=e;return F&&(t=t.replace(J," ")),p&&(t=t.replace(V,"")),r&&(t=Z(t)),n&&(t=q(t)),u&&(t=le(t)),o&&(t=Y(t,n)),i&&(t=ee(t)),s&&(t=ne(t)),c&&(t=oe(t,f)),a&&(t=se(t)),h?t.trim():t}var J=/[\u00A0\u2000-\u200A\u202F\u205F]/g,V=/[\u200B\u2060\uFEFF]/g;function Z(e){return e.replace(d,"")}function $(e){if(x.lastIndex=0,!x.test(e))return"";x.lastIndex=0;let r="";for(let n of e.matchAll(x))r+=n[0];return r}function Y(e,r=!1){let n=S(e);return r?n.replace(g,""):n.replace(g,u=>$(u))}function q(e){return e.replace(x,"")}var Q=/\r\n|\r|\n/g;function ee(e,r=`
|
|
3
|
+
`){return e.replace(Q,r)}var re=/[ \t]*(\n+)[ \t]*/g;function ne(e){return e.replace(re,"$1")}var te=/\n{2,}/g,ie=/\n{3,}/g;function oe(e,r){return r?e.replace(ie,`
|
|
4
|
+
|
|
5
|
+
`):e.replace(te,`
|
|
6
|
+
`)}var ue=/ {2,}/g;function se(e){return e.replace(ue," ")}function le(e){return e.replace(y,"")}function ae(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 C(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 fe="1.3.6";export{fe as VERSION,b as getNewlineStats,K as getRecommendedSanctifierOptions,j as inspectText,C as inspectTextDetailed,ae as revealInvisible,T as sanctifyKeyboardOnly,N as sanctifyKeyboardOnlyEmoji,w as sanctifyLoose,L as sanctifyPasted,R as sanctifyStrict,W as summonSanctifier};
|
|
@@ -7,17 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export function decodeTagText(text: string): string;
|
|
9
9
|
/**
|
|
10
|
-
* Finds runs of tag characters, variation selectors
|
|
10
|
+
* Finds runs of tag characters, variation selectors, invisible operators and reserved
|
|
11
|
+
* tag-plane code points (U+E0080–U+E00FF, U+E01F0–U+E0FFF).
|
|
11
12
|
* Consecutive characters of the same kind form one run. Two kinds of sequence are
|
|
12
13
|
* legitimate and listed under `benign` instead of `runs`:
|
|
13
14
|
* - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
|
|
14
15
|
* tags for "gbeng", "gbsct" or "gbwls" and U+E007F
|
|
15
16
|
* - a single U+FE0E or U+FE0F (text or emoji presentation) after another character
|
|
17
|
+
*
|
|
18
|
+
* `line` and `column` are counted as in inspectTextDetailed. Every run and benign
|
|
19
|
+
* span is counted; only the first `maxRuns` of each are listed. With the caps set,
|
|
20
|
+
* memory stays flat however large the input is. One pass over the string, no copy.
|
|
21
|
+
* Set both caps for untrusted input: without them, results grow with the input.
|
|
22
|
+
*
|
|
16
23
|
* @param {string} text
|
|
24
|
+
* @param {HiddenScanOptions} [options]
|
|
17
25
|
* @returns {HiddenTextScan}
|
|
18
26
|
*/
|
|
19
|
-
export function findHiddenRuns(text: string): HiddenTextScan;
|
|
20
|
-
export
|
|
27
|
+
export function findHiddenRuns(text: string, options?: HiddenScanOptions): HiddenTextScan;
|
|
28
|
+
export { VERSION } from "./version.generated.js";
|
|
29
|
+
export type HiddenRunKind = "tag" | "variation_selector" | "invisible_operator" | "reserved_tag_plane";
|
|
21
30
|
export type HiddenRun = {
|
|
22
31
|
kind: HiddenRunKind;
|
|
23
32
|
/**
|
|
@@ -33,18 +42,65 @@ export type HiddenRun = {
|
|
|
33
42
|
*/
|
|
34
43
|
count: number;
|
|
35
44
|
/**
|
|
36
|
-
* -
|
|
45
|
+
* - 1-based line of the first character (as in inspectTextDetailed)
|
|
46
|
+
*/
|
|
47
|
+
line: number;
|
|
48
|
+
/**
|
|
49
|
+
* - 1-based column of the first character, in UTF-16 units
|
|
50
|
+
*/
|
|
51
|
+
column: number;
|
|
52
|
+
/**
|
|
53
|
+
* - tag runs only: the ASCII the tags spell (see decodeTagText),
|
|
54
|
+
* at most `maxDecodedLength` UTF-16 units
|
|
37
55
|
*/
|
|
38
56
|
decoded?: string;
|
|
57
|
+
/**
|
|
58
|
+
* - tag runs only: true when `decoded` stops short of the run
|
|
59
|
+
*/
|
|
60
|
+
decodedTruncated?: boolean;
|
|
61
|
+
};
|
|
62
|
+
export type HiddenRunCounts = {
|
|
63
|
+
tag: number;
|
|
64
|
+
variation_selector: number;
|
|
65
|
+
invisible_operator: number;
|
|
66
|
+
reserved_tag_plane: number;
|
|
39
67
|
};
|
|
40
68
|
export type HiddenTextScan = {
|
|
41
69
|
/**
|
|
42
|
-
* - runs that may hide text, in order
|
|
70
|
+
* - runs that may hide text, in order, at most `maxRuns` long
|
|
43
71
|
*/
|
|
44
72
|
runs: HiddenRun[];
|
|
45
73
|
/**
|
|
46
74
|
* - [offset, end) spans of legitimate
|
|
47
|
-
* sequences (subdivision flags, a single emoji or text presentation selector)
|
|
75
|
+
* sequences (subdivision flags, a single emoji or text presentation selector), at most `maxRuns` long
|
|
48
76
|
*/
|
|
49
77
|
benign: Array<[number, number]>;
|
|
78
|
+
/**
|
|
79
|
+
* - every run per kind, listed or not
|
|
80
|
+
*/
|
|
81
|
+
counts: HiddenRunCounts;
|
|
82
|
+
/**
|
|
83
|
+
* - sum of `counts`
|
|
84
|
+
*/
|
|
85
|
+
total: number;
|
|
86
|
+
/**
|
|
87
|
+
* - every benign span, listed or not
|
|
88
|
+
*/
|
|
89
|
+
benignTotal: number;
|
|
90
|
+
/**
|
|
91
|
+
* - true when runs or benign spans were left out of the lists
|
|
92
|
+
*/
|
|
93
|
+
truncated: boolean;
|
|
94
|
+
};
|
|
95
|
+
export type HiddenScanOptions = {
|
|
96
|
+
/**
|
|
97
|
+
* - cap on listed runs, and separately on listed benign spans
|
|
98
|
+
* (non-negative integer or Infinity). Counts stay exact.
|
|
99
|
+
*/
|
|
100
|
+
maxRuns?: number;
|
|
101
|
+
/**
|
|
102
|
+
* - cap on each run's `decoded` text, in UTF-16 units.
|
|
103
|
+
* For both, any other value (negative, fractional, NaN, a string) means the default, no cap.
|
|
104
|
+
*/
|
|
105
|
+
maxDecodedLength?: number;
|
|
50
106
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,9 +6,17 @@ export type DetailedTrashReport = import("./inspectText.js").DetailedTrashReport
|
|
|
6
6
|
export type InspectDetailedOptions = import("./inspectText.js").InspectDetailedOptions;
|
|
7
7
|
export type NewlineStats = import("./inspectText.js").NewlineStats;
|
|
8
8
|
export type SanctifyOptions = import("./sanctifyText.js").SanctifyOptions;
|
|
9
|
+
export type RevealOptions = import("./reveal.js").RevealOptions;
|
|
9
10
|
import { inspectText } from './inspectText.js';
|
|
10
11
|
import { inspectTextDetailed } from './inspectText.js';
|
|
11
12
|
import { getNewlineStats } from './inspectText.js';
|
|
12
13
|
import { getRecommendedSanctifierOptions } from './inspectText.js';
|
|
13
14
|
import { summonSanctifier } from './sanctifyText.js';
|
|
14
|
-
|
|
15
|
+
import { sanctifyStrict } from './sanctifyText.js';
|
|
16
|
+
import { sanctifyLoose } from './sanctifyText.js';
|
|
17
|
+
import { sanctifyKeyboardOnlyEmoji } from './sanctifyText.js';
|
|
18
|
+
import { sanctifyKeyboardOnly } from './sanctifyText.js';
|
|
19
|
+
import { sanctifyPasted } from './sanctifyText.js';
|
|
20
|
+
import { revealInvisible } from './reveal.js';
|
|
21
|
+
import { VERSION } from './version.generated.js';
|
|
22
|
+
export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier, sanctifyStrict, sanctifyLoose, sanctifyKeyboardOnlyEmoji, sanctifyKeyboardOnly, sanctifyPasted, revealInvisible, VERSION };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @typedef {import('./inspectText.js').TrashCategory} TrashCategory */
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} RevealOptions
|
|
4
|
+
* @property {(codePoint: number, category: TrashCategory) => string} [format] - Text shown in place of
|
|
5
|
+
* each hidden character. Default: `<U+XXXX>` (at least four uppercase hex digits).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Makes hidden characters visible: every character `inspectTextDetailed` would report is
|
|
9
|
+
* replaced by a marker such as `<U+200B>`, and everything else is kept as it is,
|
|
10
|
+
* including tabs, newlines and emoji. Use it to show hidden text in logs, diffs, review
|
|
11
|
+
* tools and agent tool output. A literal `<U+200B>` already in the text looks the same as
|
|
12
|
+
* a marker; pass `format` to choose markers that can't occur in your input.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} text
|
|
15
|
+
* @param {RevealOptions} [options]
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function revealInvisible(text: string, options?: RevealOptions): string;
|
|
19
|
+
export type TrashCategory = import("./inspectText.js").TrashCategory;
|
|
20
|
+
export type RevealOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* - Text shown in place of
|
|
23
|
+
* each hidden character. Default: `<U+XXXX>` (at least four uppercase hex digits).
|
|
24
|
+
*/
|
|
25
|
+
format?: (codePoint: number, category: TrashCategory) => string;
|
|
26
|
+
};
|