text-sanctifier 1.3.9 → 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 +91 -84
- package/bin/text-sanctifier.js +214 -69
- package/dist/text-sanctifier.min.js +5 -5
- package/dist/types/hiddenScan.d.ts +30 -0
- package/dist/types/hiddenText.d.ts +40 -0
- 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 +8 -6
- package/src/hiddenScan.js +171 -0
- package/src/hiddenText.js +4 -151
- 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
|
@@ -7,28 +7,13 @@
|
|
|
7
7
|
[](https://github.com/iWhatty/text-sanctifier)
|
|
8
8
|
[](https://www.npmjs.com/package/text-sanctifier)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Find and remove invisible Unicode in web input, model-bound text and repositories. The prebuilt browser file stays under <!-- gen:budget:browser -->3 KiB<!-- /gen --> gzipped; bundlers include only the imports you use.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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`)
|
|
20
|
-
- `text-sanctifier check` / `fix` command line for CI and coding agents, with JSON or SARIF output, exit codes and pre-commit hooks
|
|
21
|
-
- `check --ai-config` finds and decodes hidden text in agent instruction files (`CLAUDE.md`, `AGENTS.md`, Cursor rules, `SKILL.md`, MCP configuration)
|
|
22
|
-
- Normalizes line endings (CRLF, CR, LF) to LF
|
|
23
|
-
- Collapses unwanted spaces and paragraphs
|
|
24
|
-
- Nukes control characters (if enabled)
|
|
25
|
-
- Smart normalization of typographic junk (quotes, dashes, bullets, full-width punctuation)
|
|
26
|
-
- Keyboard-only filtering (retain printable ASCII + full emoji sequences)
|
|
27
|
-
- Preserves ZWJ emoji clusters (the family emoji, U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466)
|
|
28
|
-
- Preserves VS16 emoji presentation variants (U+270C U+FE0F, U+203C U+FE0F)
|
|
29
|
-
- Configurable via fine-grained flags or ready-made presets
|
|
30
|
-
- Includes strict, loose, and keyboard-only modes
|
|
31
|
-
- Deterministic RegExp usage (no global `lastIndex` state leaks)
|
|
14
|
+
- **Clean:** presets and options for invisible and control characters, including the bidi controls behind Trojan Source attacks and the tag characters behind "ASCII smuggling" to LLMs, plus look-alike spaces, emoji and newlines.
|
|
15
|
+
- **Inspect:** exact offsets, lines, columns and categories; visible markers; context-aware findings and decoded hidden tag text.
|
|
16
|
+
- **CLI:** repository and AI configuration checks, fixes, JSON or SARIF output, exit codes and local pre-commit hooks.
|
|
32
17
|
|
|
33
18
|
---
|
|
34
19
|
|
|
@@ -36,65 +21,48 @@ Brutal text normalizer and invisible Unicode scrubber for modern web projects. T
|
|
|
36
21
|
|
|
37
22
|
```sh
|
|
38
23
|
pnpm add text-sanctifier
|
|
24
|
+
# or
|
|
25
|
+
npm i text-sanctifier
|
|
39
26
|
```
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Quick start
|
|
28
|
+
## Which import do I use?
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
import {
|
|
30
|
+
| Job | Import or command |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| Sanitize text with presets | `import { sanctifyPasted, sanctifyStrict } from 'text-sanctifier'` |
|
|
33
|
+
| Find and locate characters | `import { inspectTextDetailed } from 'text-sanctifier'` |
|
|
34
|
+
| Emoji-aware findings | `import { inspectTextInContext } from 'text-sanctifier/context'` |
|
|
35
|
+
| Text headed to a model | `import { findHiddenRuns, decodeTagText } from 'text-sanctifier/hidden'` |
|
|
36
|
+
| No build step | `import { sanctifyPasted } from 'text-sanctifier/browser'` (CDN example below) |
|
|
37
|
+
| Repository checks | `npx text-sanctifier check` |
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
purgeInvisibleChars: true,
|
|
52
|
-
purgeEmojis: true,
|
|
53
|
-
collapseSpaces: true,
|
|
54
|
-
collapseNewLines: true,
|
|
55
|
-
preserveParagraphs: true,
|
|
56
|
-
finalTrim: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const output = clean(rawText);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Strict preset:**
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
const output = summonSanctifier.strict(rawText);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Loose preset:**
|
|
69
|
-
|
|
70
|
-
```js
|
|
71
|
-
const output = summonSanctifier.loose(rawText);
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Keyboard-only (no emojis):**
|
|
75
|
-
|
|
76
|
-
```js
|
|
77
|
-
const output = summonSanctifier.keyboardOnly(userInput);
|
|
78
|
-
```
|
|
39
|
+
## Quick start
|
|
79
40
|
|
|
80
|
-
|
|
41
|
+
### Web
|
|
81
42
|
|
|
82
43
|
```js
|
|
83
|
-
|
|
44
|
+
import { sanctifyPasted } from 'text-sanctifier';
|
|
45
|
+
const input = 'Hello\u202Fworld\u200B';
|
|
46
|
+
const clean = sanctifyPasted(input); // 'Hello world'
|
|
47
|
+
document.querySelector('#preview').textContent = clean;
|
|
84
48
|
```
|
|
85
49
|
|
|
86
|
-
|
|
50
|
+
Without a build step, use the browser file from a CDN:
|
|
87
51
|
|
|
88
|
-
```
|
|
89
|
-
|
|
52
|
+
```html
|
|
53
|
+
<div id="preview"></div>
|
|
54
|
+
<script type="module">
|
|
55
|
+
import { sanctifyPasted } from 'https://cdn.jsdelivr.net/npm/text-sanctifier/dist/text-sanctifier.min.js';
|
|
56
|
+
document.querySelector('#preview').textContent = sanctifyPasted('Hello\u200B world');
|
|
57
|
+
</script>
|
|
90
58
|
```
|
|
91
59
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
|
-
import { revealInvisible } from 'text-sanctifier';
|
|
60
|
+
### Agents/CI
|
|
96
61
|
|
|
97
|
-
|
|
62
|
+
```sh
|
|
63
|
+
npx text-sanctifier check src docs
|
|
64
|
+
npx text-sanctifier check --ai-config
|
|
65
|
+
# check exits: 0 clean, 1 findings, 2 usage or I/O error
|
|
98
66
|
```
|
|
99
67
|
|
|
100
68
|
---
|
|
@@ -104,7 +72,7 @@ revealInvisible(text); // 'Hello<U+200B>world'
|
|
|
104
72
|
- **Not an HTML/XSS sanitizer.** This library normalizes and filters plain text.
|
|
105
73
|
- If you need to render **untrusted content**, render it as text (e.g. `textContent`), not HTML (`innerHTML`).
|
|
106
74
|
- If you need to sanitize **HTML**, use a dedicated HTML sanitizer (e.g. DOMPurify / sanitize-html).
|
|
107
|
-
- Like any text-processing library, extremely large untrusted inputs can be used for CPU/DoS pressure;
|
|
75
|
+
- Like any text-processing library, extremely large untrusted inputs can be used for CPU/DoS pressure; set input size limits with [`maxLength` and `onMaxLength`](#sanctifyoptions) in high-risk environments. Inspection caps limit output, not input length or scan time.
|
|
108
76
|
|
|
109
77
|
---
|
|
110
78
|
|
|
@@ -119,9 +87,41 @@ Two options handle spaces and zero-width characters gently, and both run before
|
|
|
119
87
|
- `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
88
|
- `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
89
|
|
|
90
|
+
### `SanctifyOptions`
|
|
91
|
+
|
|
92
|
+
Used by `summonSanctifier(options)`. All boolean options default to `false`. Processing follows table order after the input-length check.
|
|
93
|
+
|
|
94
|
+
| Option | Behavior |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `normalizeSpaces` | Replace U+00A0, U+2000-U+200A, U+202F and U+205F with ordinary spaces; keep U+3000. |
|
|
97
|
+
| `purgeZeroWidthSpaces` | Remove only U+200B, U+2060 and U+FEFF. |
|
|
98
|
+
| `purgeInvisibleChars` | Remove characters marked `purgeInvisibleChars` in the category table, including emoji joiners and selectors. |
|
|
99
|
+
| `purgeEmojis` | Remove matched emoji sequences. |
|
|
100
|
+
| `nukeControls` | Remove NUL, non-whitespace C0 controls, DEL, C1 controls and bidi controls; keep tab, LF and CR. |
|
|
101
|
+
| `keyboardOnlyFilter` | Normalize typographic punctuation, then keep printable ASCII, CR/LF and matched emoji sequences; with `purgeEmojis`, keep no emoji. Deletes non-ASCII letters. |
|
|
102
|
+
| `normalizeNewlines` | Convert CRLF and CR to LF. |
|
|
103
|
+
| `trimSpacesAroundNewlines` | Remove spaces and tabs immediately around LF runs. |
|
|
104
|
+
| `collapseNewLines` | Collapse runs of two or more LFs to one, or three or more to two with `preserveParagraphs`. |
|
|
105
|
+
| `preserveParagraphs` | Keep double LF breaks when `collapseNewLines` is enabled; otherwise no effect. |
|
|
106
|
+
| `collapseSpaces` | Collapse runs of ordinary spaces to one; keep tabs and newlines. |
|
|
107
|
+
| `finalTrim` | Apply JavaScript `.trim()` to the result. |
|
|
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 at most the cap before processing, never through a surrogate pair. `'noop'`: return original input unchanged. Invalid values use `'throw'`. |
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
import { summonSanctifier } from 'text-sanctifier';
|
|
113
|
+
const clean = summonSanctifier({ purgeInvisibleChars: true, maxLength: 10000, onMaxLength: 'throw' });
|
|
114
|
+
```
|
|
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
|
+
|
|
118
|
+
### Errors
|
|
119
|
+
|
|
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'`.
|
|
121
|
+
|
|
122
122
|
### Presets as standalone imports
|
|
123
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 <!-- 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 -->).
|
|
125
125
|
|
|
126
126
|
```js
|
|
127
127
|
import { sanctifyPasted } from 'text-sanctifier';
|
|
@@ -131,19 +131,19 @@ const output = sanctifyPasted(clipboardText);
|
|
|
131
131
|
|
|
132
132
|
### `summonSanctifier.strict`
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
Purges invisible characters, emoji and non-whitespace controls. Normalizes CRLF and CR to LF, removes spaces/tabs around LF, collapses blank lines to a single LF, collapses ordinary spaces and trims the ends. Single line breaks remain.
|
|
135
135
|
|
|
136
136
|
### `summonSanctifier.loose`
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Normalizes spacing and newlines while preserving emoji and paragraph breaks (LF runs collapse to at most two). Keeps zero-width characters such as U+200B, U+200C, U+200D and U+2060; it does not enable `purgeInvisibleChars`.
|
|
139
139
|
|
|
140
140
|
### `summonSanctifier.keyboardOnly`
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Normalizes typographic punctuation, then keeps printable ASCII and line breaks; removes emoji and non-ASCII letters.
|
|
143
143
|
|
|
144
144
|
### `summonSanctifier.keyboardOnlyEmoji`
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Normalizes typographic punctuation, then keeps printable ASCII, line breaks and matched emoji sequences, including ZWJ clusters and emoji presentation variants. Both keyboard-only presets delete non-ASCII letters: `sanctifyKeyboardOnlyEmoji('caf\u00E9')` returns `'caf'`, not an ASCII transliteration.
|
|
147
147
|
|
|
148
148
|
### `summonSanctifier.pasted`
|
|
149
149
|
|
|
@@ -189,7 +189,7 @@ const report = inspectText(input);
|
|
|
189
189
|
*/
|
|
190
190
|
```
|
|
191
191
|
|
|
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()`.
|
|
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.
|
|
193
193
|
|
|
194
194
|
### `inspectTextDetailed(text: string, options?: { maxFindings?: number }): DetailedTrashReport`
|
|
195
195
|
|
|
@@ -228,7 +228,11 @@ truncated: false
|
|
|
228
228
|
| `tag` | U+E0000–U+E007F (tag characters, used for "ASCII smuggling" of hidden instructions) | `purgeInvisibleChars` |
|
|
229
229
|
<!-- categories:end -->
|
|
230
230
|
|
|
231
|
-
`
|
|
231
|
+
`zero_width` also flags U+200D (zero width joiner) inside emoji ZWJ sequences such as U+1F468 U+200D U+1F469 U+200D U+1F467, so a finding there is not necessarily hidden text. Use [`text-sanctifier/context`](#text-sanctifiercontext-inspecttextincontexttext-options) to mark supported legitimate sequences as benign. (`purgeInvisibleChars` removes those joiners too, splitting the emoji; `keyboardOnlyEmoji` keeps the sequences whole.) The same holds for `variation_selector`, which flags VS16 (U+FE0F) in emoji such as U+270C U+FE0F and the ideographic variation selectors in CJK names, and for `tag`, which flags the tag characters inside subdivision flags such as the England flag (U+1F3F4 followed by tags). `purgeInvisibleChars` strips them all; `keyboardOnlyEmoji` keeps VS16. The Mongolian free variation selectors (U+180B–U+180D, U+180F) are also `variation_selector`: they are legitimate in Mongolian text, which `purgeInvisibleChars` can change, so don't purge Mongolian text with it.
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
`codePoint` is the full code point, so a tag character reports `0xE0041`, not a surrogate. `offset` is a 0-based UTF-16 index (for a code point above U+FFFF, that of its high surrogate); `line` and `column` are 1-based, with columns in UTF-16 code units and lines ending at LF, CR or CRLF. Every occurrence is counted; only the first `maxFindings` (default 100, `Infinity` for all; invalid values such as negative numbers, fractions, strings, `NaN` or `null` fall back to 100) are listed, and `truncated` says when some were not. The scan is one pass with no regex and no copy of the input.
|
|
232
236
|
|
|
233
237
|
**Choosing `maxFindings`:** the scan itself uses almost no memory, but every listed finding is an object, so the cost of `maxFindings` grows with the number of findings, not the size of the input. 16 MB of non-breaking spaces is 16.7 million findings. With a cap of 20, it scans in about 80 ms and uses 0.2 MB (`npm run bench` measures it on your machine). With `Infinity`, expect roughly 100–200 bytes per finding: about 1.5 GB for 8 million. On untrusted input, keep a cap. Use `maxFindings: 0` for a counts-only report: it builds no finding objects, and `counts`, `total` and `truncated` stay exact.
|
|
234
238
|
|
|
@@ -242,10 +246,6 @@ import { getNewlineStats } from 'text-sanctifier';
|
|
|
242
246
|
getNewlineStats('a\r\nb\n'); // { crlf: 1, cr: 0, lf: 1, types: ['CRLF', 'LF'], mixed: true }
|
|
243
247
|
```
|
|
244
248
|
|
|
245
|
-
`zero_width` also flags U+200D (zero width joiner) inside emoji ZWJ sequences such as U+1F468 U+200D U+1F469 U+200D U+1F467, so a finding there is not necessarily hidden text. Filter by neighbouring code points if you need to tell them apart. (`purgeInvisibleChars` removes those joiners too, splitting the emoji; `keyboardOnlyEmoji` keeps the sequences whole.) The same holds for `variation_selector`, which flags VS16 (U+FE0F) in emoji such as U+270C U+FE0F and the ideographic variation selectors in CJK names, and for `tag`, which flags the tag characters inside subdivision flags such as the England flag (U+1F3F4 followed by tags). `purgeInvisibleChars` strips them all; `keyboardOnlyEmoji` keeps VS16. The Mongolian free variation selectors (U+180B–U+180D, U+180F) are also `variation_selector`: they are legitimate in Mongolian text, which `purgeInvisibleChars` can change, so don't purge Mongolian text with it.
|
|
246
|
-
|
|
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.
|
|
248
|
-
|
|
249
249
|
### `text-sanctifier/hidden`: `findHiddenRuns(text, options?)` and `decodeTagText(text)`
|
|
250
250
|
|
|
251
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.
|
|
@@ -261,7 +261,7 @@ const { runs, benign, counts, total, truncated } = findHiddenRuns(text, { maxRun
|
|
|
261
261
|
|
|
262
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
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
|
|
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. Only non-negative integers and `Infinity` are valid; invalid values (negative, fractional, a string, `NaN` or `null`) also mean 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.
|
|
265
265
|
- `decodeTagText` turns U+E0020–U+E007E into the ASCII they shadow and writes any other character as `<U+XXXX>`.
|
|
266
266
|
- Zero-width, bidi and other invisible characters are not runs; use `inspectTextDetailed` for those.
|
|
267
267
|
- `codePointCounts` has the same keys as `counts` and counts the code points in those runs, so long runs weigh more than short ones; `supplementarySelectorCount` counts U+E0100–U+E01EF on their own, apart from the emoji selectors U+FE00–U+FE0F. Both leave out benign spans and stay exact under `maxRuns`.
|
|
@@ -299,6 +299,9 @@ U+200B, U+2060, U+FEFF, bidi controls and all other tags are never benign, and n
|
|
|
299
299
|
The package ships a `text-sanctifier` command for CI and coding agents, which cannot see invisible characters in their own tool output. It runs on Node 18+ and has no dependencies.
|
|
300
300
|
|
|
301
301
|
```sh
|
|
302
|
+
npx text-sanctifier --help
|
|
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
|
|
@@ -349,6 +352,7 @@ skills/review/SKILL.md:2:3 U+FE00 run of 3 variation selectors
|
|
|
349
352
|
- `ruleId` is the category, and `tool.driver.rules` lists every category.
|
|
350
353
|
- `level`: `error` for `bidi_control` and `tag`, `note` for `nonbreaking_space`, `warning` for the rest.
|
|
351
354
|
- Each location has the file (relative to `%SRCROOT%`, the current directory), `startLine`, `startColumn` and `endColumn`. Columns count UTF-16 code units, as in `inspectTextDetailed` (`columnKind: "utf16CodeUnits"`).
|
|
355
|
+
- Each result has a `partialFingerprints` hash based on its rule, relative path, line content and occurrence on that line. Adding unrelated lines above does not change it.
|
|
352
356
|
- With `--ai-config`, the message of the first result in a run describes the run and its decoded text.
|
|
353
357
|
- `--max-findings` caps results per file as usual. `runs[0].properties` holds `total`, `filesScanned`, `filesWithFindings`, `truncated` (files with findings left out) and `skipped`.
|
|
354
358
|
|
|
@@ -360,13 +364,13 @@ The repository publishes a [pre-commit](https://pre-commit.com) hook manifest, s
|
|
|
360
364
|
# .pre-commit-config.yaml
|
|
361
365
|
repos:
|
|
362
366
|
- repo: https://github.com/iWhatty/text-sanctifier
|
|
363
|
-
rev: v1.3.
|
|
367
|
+
rev: v1.3.11 # a release tag
|
|
364
368
|
hooks:
|
|
365
369
|
- id: text-sanctifier # every staged text file
|
|
366
370
|
- id: text-sanctifier-ai-config # agent instruction files, with --ai-config
|
|
367
371
|
```
|
|
368
372
|
|
|
369
|
-
- `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.
|
|
370
374
|
- `text-sanctifier-ai-config` runs `text-sanctifier check --ai-config` on staged files that match the `--ai-config` file list.
|
|
371
375
|
- A hook fails the commit when it finds something (exit code 1). Neither hook changes files; run `text-sanctifier fix` yourself.
|
|
372
376
|
|
|
@@ -376,6 +380,7 @@ Rewrites files in place and reports what it replaced and removed.
|
|
|
376
380
|
|
|
377
381
|
- `--normalize-spaces`: `normalizeSpaces` (look-alike spaces become ordinary spaces)
|
|
378
382
|
- `--purge-invisible`: `purgeInvisibleChars`
|
|
383
|
+
- `--keep-benign`: preserve contextually legitimate joiners, selectors and flag tags when purging invisible characters. Works with the default cleaning flags too.
|
|
379
384
|
- `--nuke-controls`: `nukeControls`
|
|
380
385
|
- `--normalize-newlines`: `normalizeNewlines` (CRLF and CR to LF)
|
|
381
386
|
- 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.
|
|
@@ -383,7 +388,7 @@ Rewrites files in place and reports what it replaced and removed.
|
|
|
383
388
|
- `--dry-run`: write nothing; report what would change.
|
|
384
389
|
- `--json` (or `--format json`): `{ files, dryRun, filesScanned, filesChanged, skipped }`, where each file is `{ path, spacesReplaced, removed, removedTotal, newlinesNormalized }`.
|
|
385
390
|
|
|
386
|
-
Apart from look-alike spaces, `fix` deletes characters.
|
|
391
|
+
Apart from look-alike spaces, `fix` deletes characters. By default, emoji lose their zero width joiners and variation selectors, and so does Mongolian text. Use `--keep-benign` to preserve recognized legitimate sequences, or run `check` or `fix --dry-run` first on prose. To make hidden characters visible without changing a file, use `reveal`.
|
|
387
392
|
|
|
388
393
|
### `reveal [paths...]`
|
|
389
394
|
|
|
@@ -398,9 +403,11 @@ git diff | npx text-sanctifier reveal
|
|
|
398
403
|
|
|
399
404
|
- Paths can be files, directories or globs (`*`, `**`, `?`, `[...]`, `{a,b}`). Quote globs so the shell passes them through.
|
|
400
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.
|
|
401
|
-
- 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`.
|
|
402
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`.
|
|
403
|
-
-
|
|
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.
|
|
404
411
|
|
|
405
412
|
### Exit codes
|
|
406
413
|
|
|
@@ -408,7 +415,7 @@ git diff | npx text-sanctifier reveal
|
|
|
408
415
|
| --- | --- | --- | --- |
|
|
409
416
|
| 0 | nothing found | done (or nothing to do) | done |
|
|
410
417
|
| 1 | findings | `--dry-run` with changes pending | not used |
|
|
411
|
-
| 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` |
|
|
412
419
|
|
|
413
420
|
---
|
|
414
421
|
|