text-sanctifier 1.3.8 → 1.3.9

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.
@@ -378,18 +378,22 @@ function collectInputs(args, cwd, warn, aiConfig = false) {
378
378
  // --- Reading ---
379
379
 
380
380
  const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
381
+ const utf8Lenient = new TextDecoder('utf-8', { ignoreBOM: true });
381
382
 
382
383
  /**
383
- * Decodes UTF-8, keeping a leading BOM. Returns null for binary or non-UTF-8 data.
384
+ * Decodes UTF-8, keeping a leading BOM. With `sniff`, data with a NUL in its first
385
+ * BINARY_SNIFF_BYTES is binary and not decoded. Bytes that are not UTF-8 become U+FFFD
386
+ * and set `lossy`, so one bad byte cannot hide the rest of a file from `check`.
384
387
  * @param {Uint8Array} bytes
385
- * @returns {{ text: string|null, reason?: string }}
388
+ * @param {boolean} sniff
389
+ * @returns {{ text: string|null, reason?: string, lossy?: boolean }}
386
390
  */
387
- function decode(bytes) {
388
- if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return { text: null, reason: 'binary' };
391
+ function decode(bytes, sniff) {
392
+ if (sniff && bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return { text: null, reason: 'binary' };
389
393
  try {
390
394
  return { text: utf8.decode(bytes) };
391
395
  } catch {
392
- return { text: null, reason: 'not UTF-8' };
396
+ return { text: utf8Lenient.decode(bytes), lossy: true };
393
397
  }
394
398
  }
395
399
 
@@ -612,6 +616,8 @@ async function main(argv) {
612
616
 
613
617
  const cwd = process.cwd();
614
618
  let paths = opts.paths;
619
+ // --ai-config with no paths searches the current directory even when stdin is not a
620
+ // terminal, so a CI job with no stdin still scans the repository; `-` reads stdin.
615
621
  if (paths.length === 0 && opts.aiConfig) {
616
622
  paths = ['.'];
617
623
  } else if (paths.length === 0) {
@@ -627,6 +633,8 @@ async function main(argv) {
627
633
  let errors = pathErrors;
628
634
  /** @type {Array<{ path: string, reason: string }>} */
629
635
  const skipped = [];
636
+ /** @type {string[]} */
637
+ const notUtf8 = [];
630
638
 
631
639
  /**
632
640
  * Reads every input in order, calling `visit(label, text, input)` for each decodable one.
@@ -643,12 +651,24 @@ async function main(argv) {
643
651
  errors++;
644
652
  continue;
645
653
  }
646
- const { text, reason } = decode(bytes);
654
+ // Files that look binary are skipped. Stdin and AI configuration files are text
655
+ // by intent, so a NUL in one is a finding, not a reason to skip it.
656
+ const { text, reason, lossy } = decode(bytes, input.path !== STDIN && !opts.aiConfig);
647
657
  if (text === null) {
648
658
  skipped.push({ path: label, reason });
649
659
  if (input.explicit) warn(`${label}: skipped (${reason})`);
650
660
  continue;
651
661
  }
662
+ if (lossy) {
663
+ // fix never rewrites bytes it could not decode
664
+ if (opts.command === 'fix') {
665
+ skipped.push({ path: label, reason: 'not UTF-8' });
666
+ warn(`${label}: skipped (not UTF-8)`);
667
+ continue;
668
+ }
669
+ notUtf8.push(label);
670
+ warn(`${label}: not valid UTF-8; checked with invalid bytes read as U+FFFD`);
671
+ }
652
672
  visit(label, text, input);
653
673
  }
654
674
  };
@@ -691,14 +711,17 @@ async function main(argv) {
691
711
 
692
712
  const kind = opts.aiConfig ? 'AI configuration file' : 'file';
693
713
  if (opts.format === 'json') {
694
- const summary = { files, total, filesScanned: scanned, filesWithFindings: files.length, skipped };
714
+ const summary = { files, total, filesScanned: scanned, filesWithFindings: files.length, skipped, notUtf8 };
695
715
  if (opts.aiConfig) summary.hiddenRunCount = hiddenRunCount;
696
716
  out(JSON.stringify(summary, null, 2));
697
717
  } else if (opts.format === 'sarif') {
698
718
  out(JSON.stringify(sarifLog(files, { cwd, total, filesScanned: scanned, skipped, succeeded: errors === 0 }), null, 2));
699
719
  } else {
700
720
  for (const line of lines) out(line);
701
- if (opts.aiConfig && scanned === 0 && skipped.length === 0) out('No AI configuration files found.');
721
+ if (opts.aiConfig && scanned === 0 && skipped.length === 0) {
722
+ out('No AI configuration files found.');
723
+ if (opts.paths.length === 0 && !process.stdin.isTTY) out('To check piped text, pass "-": text-sanctifier check --ai-config -');
724
+ }
702
725
  else if (total === 0) out(`No ${opts.aiConfig ? 'hidden text, ' : ''}invisible or control characters in ${plural(scanned, kind)}.`);
703
726
  else out(`${plural(total, 'finding')}${opts.aiConfig ? ` (${plural(hiddenRunCount, 'hidden run')})` : ''} in ${files.length} of ${plural(scanned, kind)}.`);
704
727
  }
@@ -3,4 +3,4 @@ var y=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u2
3
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
4
 
5
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.8";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};
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.9";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};
@@ -13,7 +13,10 @@ export function decodeTagText(text: string): string;
13
13
  * legitimate and listed under `benign` instead of `runs`:
14
14
  * - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
15
15
  * tags for "gbeng", "gbsct" or "gbwls" and U+E007F
16
- * - a single U+FE0E or U+FE0F (text or emoji presentation) after another character
16
+ * - a single U+FE0E or U+FE0F (text or emoji presentation) after an
17
+ * Extended_Pictographic character, or between #, * or a digit and U+20E3 (a keycap),
18
+ * the same rule as text-sanctifier/context. After a letter it is a run: one selector
19
+ * per letter can carry a bit each.
17
20
  *
18
21
  * `line` and `column` are counted as in inspectTextDetailed. Every run and benign
19
22
  * span is counted; only the first `maxRuns` of each are listed. With the caps set,
@@ -52,7 +52,7 @@ export function sanctifyPasted(text: string): string;
52
52
  * Brutal text normalizer and invisible trash scrubber,
53
53
  * configurable to kill whatever ghosts you want dead.
54
54
  *
55
- * ⚠️ Note: This is plain-text normalization/filtering — not an HTML/XSS sanitizer.
55
+ * Note: This is plain-text normalization/filtering — not an HTML/XSS sanitizer.
56
56
  *
57
57
  * @param {string} text
58
58
  * @param {boolean} [purgeInvisibleChars=false] - Remove ZWSP, NBSP, bidi, tags, variation selectors, etc.
@@ -1,2 +1,2 @@
1
1
  /** The installed version of text-sanctifier, as in its package.json. */
2
- export const VERSION: "1.3.8";
2
+ export const VERSION: "1.3.9";
package/package.json CHANGED
@@ -1,83 +1,89 @@
1
- {
2
- "name": "text-sanctifier",
3
- "version": "1.3.8",
4
- "type": "module",
5
- "description": "A brutal text normalizer and invisible trash scrubber for modern web projects.",
6
- "engines": {
7
- "node": ">=18"
8
- },
9
- "main": "./src/index.js",
10
- "module": "./src/index.js",
11
- "browser": "./dist/text-sanctifier.min.js",
12
- "bin": {
13
- "text-sanctifier": "bin/text-sanctifier.js"
14
- },
15
- "files": [
16
- "src",
17
- "bin",
18
- "dist/text-sanctifier.min.js",
19
- "dist/types",
20
- "LICENSE",
21
- "README.md",
22
- "ADDITIONAL_TERMS.md",
23
- "robots.txt"
24
- ],
25
- "exports": {
26
- ".": {
27
- "types": "./dist/types/index.d.ts",
28
- "default": "./src/index.js"
29
- },
30
- "./hidden": {
31
- "types": "./dist/types/hiddenText.d.ts",
32
- "default": "./src/hiddenText.js"
33
- },
34
- "./context": {
35
- "types": "./dist/types/context.d.ts",
36
- "default": "./src/context.js"
37
- },
38
- "./browser": {
39
- "types": "./dist/types/index.d.ts",
40
- "default": "./dist/text-sanctifier.min.js"
41
- }
42
- },
43
- "types": "./dist/types/index.d.ts",
44
- "sideEffects": false,
45
- "scripts": {
46
- "gen": "node scripts/gen-categories.js && node scripts/gen-version.js && node scripts/gen-virama.js",
47
- "gen:check": "node scripts/gen-categories.js --check && node scripts/gen-version.js --check && node scripts/gen-virama.js --check",
48
- "build": "node scripts/build.js",
49
- "build:types": "tsc -p tsconfig.types.json",
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",
52
- "test-min": "node tests/sanctifyText.test.min.js",
53
- "ci": "npm run gen:check && npm run test && npm run build && npm run size && npm run build:types && npm run test:types && npm run test-min",
54
- "size": "node scripts/check-size.js"
55
- },
56
- "keywords": [
57
- "text",
58
- "sanitize",
59
- "normalize",
60
- "invisible",
61
- "scrubber",
62
- "unicode",
63
- "clean",
64
- "sanctify"
65
- ],
66
- "author": "WATT3D <WATT3D@protonmail.com>",
67
- "license": "SEE LICENSE IN LICENSE",
68
- "repository": {
69
- "type": "git",
70
- "url": "git+https://github.com/iWhatty/text-sanctifier.git"
71
- },
72
- "bugs": {
73
- "url": "https://github.com/iWhatty/text-sanctifier/issues"
74
- },
75
- "homepage": "https://github.com/iWhatty/text-sanctifier#readme",
76
- "directories": {
77
- "test": "tests"
78
- },
79
- "devDependencies": {
80
- "esbuild": "^0.25.3",
81
- "typescript": "^5.9.3"
82
- }
83
- }
1
+ {
2
+ "name": "text-sanctifier",
3
+ "version": "1.3.9",
4
+ "type": "module",
5
+ "description": "A brutal text normalizer and invisible trash scrubber for modern web projects.",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "main": "./src/index.js",
10
+ "module": "./src/index.js",
11
+ "browser": "./dist/text-sanctifier.min.js",
12
+ "bin": {
13
+ "text-sanctifier": "bin/text-sanctifier.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "bin",
18
+ "dist/text-sanctifier.min.js",
19
+ "dist/types",
20
+ "LICENSE",
21
+ "README.md",
22
+ "ADDITIONAL_TERMS.md",
23
+ "robots.txt"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/types/index.d.ts",
28
+ "default": "./src/index.js"
29
+ },
30
+ "./hidden": {
31
+ "types": "./dist/types/hiddenText.d.ts",
32
+ "default": "./src/hiddenText.js"
33
+ },
34
+ "./context": {
35
+ "types": "./dist/types/context.d.ts",
36
+ "default": "./src/context.js"
37
+ },
38
+ "./browser": {
39
+ "types": "./dist/types/index.d.ts",
40
+ "default": "./dist/text-sanctifier.min.js"
41
+ }
42
+ },
43
+ "types": "./dist/types/index.d.ts",
44
+ "sideEffects": false,
45
+ "scripts": {
46
+ "gen": "node scripts/gen-categories.js && node scripts/gen-version.js && node scripts/gen-virama.js && node scripts/gen-docs.js",
47
+ "gen:check": "node scripts/gen-categories.js --check && node scripts/gen-version.js --check && node scripts/gen-virama.js --check && node scripts/gen-docs.js --check",
48
+ "build": "node scripts/build.js",
49
+ "build:types": "tsc -p tsconfig.types.json",
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",
52
+ "test-min": "node tests/sanctifyText.test.min.js",
53
+ "check:self": "node bin/text-sanctifier.js check .",
54
+ "check:names": "node scripts/check-names.js",
55
+ "ci": "npm run gen:check && npm run check:names && 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
+ "size": "node scripts/check-size.js",
57
+ "bench": "node --expose-gc scripts/bench.js",
58
+ "release:prep": "node scripts/release.js prep",
59
+ "release:verify": "node scripts/release.js verify",
60
+ "hooks:install": "git config core.hooksPath scripts/hooks"
61
+ },
62
+ "keywords": [
63
+ "text",
64
+ "sanitize",
65
+ "normalize",
66
+ "invisible",
67
+ "scrubber",
68
+ "unicode",
69
+ "clean",
70
+ "sanctify"
71
+ ],
72
+ "author": "WATT3D <WATT3D@protonmail.com>",
73
+ "license": "SEE LICENSE IN LICENSE",
74
+ "repository": {
75
+ "type": "git",
76
+ "url": "git+https://github.com/iWhatty/text-sanctifier.git"
77
+ },
78
+ "bugs": {
79
+ "url": "https://github.com/iWhatty/text-sanctifier/issues"
80
+ },
81
+ "homepage": "https://github.com/iWhatty/text-sanctifier#readme",
82
+ "directories": {
83
+ "test": "tests"
84
+ },
85
+ "devDependencies": {
86
+ "esbuild": "^0.25.3",
87
+ "typescript": "^5.9.3"
88
+ }
89
+ }
package/robots.txt CHANGED
@@ -1,79 +1,79 @@
1
- # WATT3D — AI scraping policy
2
- #
3
- # This file enumerates known AI / LLM training and inference crawlers
4
- # that the WATT3D AI Training Rider does not consent to. See
5
- # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
- #
7
- # Honoring robots.txt is voluntary. The rider applies whether or not
8
- # a bot respects this file. The enumeration is non-exhaustive — bots
9
- # not listed here are still bound by the rider if they perform
10
- # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
-
12
- User-agent: GPTBot
13
- Disallow: /
14
-
15
- User-agent: ChatGPT-User
16
- Disallow: /
17
-
18
- User-agent: OAI-SearchBot
19
- Disallow: /
20
-
21
- User-agent: anthropic-ai
22
- Disallow: /
23
-
24
- User-agent: ClaudeBot
25
- Disallow: /
26
-
27
- User-agent: Claude-Web
28
- Disallow: /
29
-
30
- User-agent: Claude-SearchBot
31
- Disallow: /
32
-
33
- User-agent: Google-Extended
34
- Disallow: /
35
-
36
- User-agent: Applebot-Extended
37
- Disallow: /
38
-
39
- User-agent: Meta-ExternalAgent
40
- Disallow: /
41
-
42
- User-agent: Meta-ExternalFetcher
43
- Disallow: /
44
-
45
- User-agent: FacebookBot
46
- Disallow: /
47
-
48
- User-agent: CCBot
49
- Disallow: /
50
-
51
- User-agent: PerplexityBot
52
- Disallow: /
53
-
54
- User-agent: Perplexity-User
55
- Disallow: /
56
-
57
- User-agent: cohere-ai
58
- Disallow: /
59
-
60
- User-agent: Bytespider
61
- Disallow: /
62
-
63
- User-agent: Amazonbot
64
- Disallow: /
65
-
66
- User-agent: Omgilibot
67
- Disallow: /
68
-
69
- User-agent: AI2Bot
70
- Disallow: /
71
-
72
- User-agent: Diffbot
73
- Disallow: /
74
-
75
- # Everyone else: full access. Search engines and human-driven crawlers
76
- # remain welcome.
77
-
78
- User-agent: *
79
- Allow: /
1
+ # WATT3D — AI scraping policy
2
+ #
3
+ # This file enumerates known AI / LLM training and inference crawlers
4
+ # that the WATT3D AI Training Rider does not consent to. See
5
+ # ./ADDITIONAL_TERMS.md for the legal scope and definitions.
6
+ #
7
+ # Honoring robots.txt is voluntary. The rider applies whether or not
8
+ # a bot respects this file. The enumeration is non-exhaustive — bots
9
+ # not listed here are still bound by the rider if they perform
10
+ # Training Use as defined in ADDITIONAL_TERMS.md §1.
11
+
12
+ User-agent: GPTBot
13
+ Disallow: /
14
+
15
+ User-agent: ChatGPT-User
16
+ Disallow: /
17
+
18
+ User-agent: OAI-SearchBot
19
+ Disallow: /
20
+
21
+ User-agent: anthropic-ai
22
+ Disallow: /
23
+
24
+ User-agent: ClaudeBot
25
+ Disallow: /
26
+
27
+ User-agent: Claude-Web
28
+ Disallow: /
29
+
30
+ User-agent: Claude-SearchBot
31
+ Disallow: /
32
+
33
+ User-agent: Google-Extended
34
+ Disallow: /
35
+
36
+ User-agent: Applebot-Extended
37
+ Disallow: /
38
+
39
+ User-agent: Meta-ExternalAgent
40
+ Disallow: /
41
+
42
+ User-agent: Meta-ExternalFetcher
43
+ Disallow: /
44
+
45
+ User-agent: FacebookBot
46
+ Disallow: /
47
+
48
+ User-agent: CCBot
49
+ Disallow: /
50
+
51
+ User-agent: PerplexityBot
52
+ Disallow: /
53
+
54
+ User-agent: Perplexity-User
55
+ Disallow: /
56
+
57
+ User-agent: cohere-ai
58
+ Disallow: /
59
+
60
+ User-agent: Bytespider
61
+ Disallow: /
62
+
63
+ User-agent: Amazonbot
64
+ Disallow: /
65
+
66
+ User-agent: Omgilibot
67
+ Disallow: /
68
+
69
+ User-agent: AI2Bot
70
+ Disallow: /
71
+
72
+ User-agent: Diffbot
73
+ Disallow: /
74
+
75
+ # Everyone else: full access. Search engines and human-driven crawlers
76
+ # remain welcome.
77
+
78
+ User-agent: *
79
+ Allow: /