text-sanctifier 1.3.1 → 1.3.3
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 +349 -277
- package/bin/text-sanctifier.js +753 -519
- package/dist/text-sanctifier.min.js +15 -14
- package/dist/types/categories.generated.d.ts +27 -0
- package/dist/types/hiddenText.d.ts +50 -0
- package/dist/types/inspectText.d.ts +7 -7
- package/dist/types/sanctifyText.d.ts +3 -29
- package/package.json +80 -74
- package/src/categories.generated.js +223 -0
- package/src/hiddenText.js +120 -0
- package/src/inspectText.js +343 -390
- package/src/sanctifyText.js +506 -511
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
l&&(a=a.
|
|
5
|
-
function w(a,c=!1){a=E(a);return c?a.replace(D,""):a.replace(D,d=>{
|
|
6
|
-
function E(a){return a.replace(F,"'").replace(G,'"').replace(H,"-").replace(I,"...").replace(J,"*").replace(K,c=>String.fromCharCode(c.charCodeAt(0)-65248))}var
|
|
7
|
-
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(
|
|
9
|
-
|
|
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},
|
|
11
|
-
l=k+1);continue}const m=k;var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function
|
|
1
|
+
var n=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g,q=/[\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;
|
|
2
|
+
function t(a={}){const c=!!a.purgeInvisibleChars,d=!!a.purgeEmojis,f=!!a.nukeControls,h=!!a.keyboardOnlyFilter,g=!!a.normalizeNewlines,l=!!a.trimSpacesAroundNewlines,b=!!a.collapseNewLines,e=!!a.preserveParagraphs,k=!!a.collapseSpaces,r=!!a.finalTrim,m=Number.isFinite(a.maxLength)&&0<=a.maxLength?a.maxLength:Infinity,p="truncate"===a.g||"noop"===a.g||"throw"===a.g?a.g:"throw";return A=>u(A,c,d,f,h,g,l,b,e,k,r,m,p)}t.strict=a=>u(a,!0,!0,!0,!1,!0,!0,!0,!1,!0,!0);
|
|
3
|
+
t.loose=a=>u(a,!1,!1,!1,!1,!0,!0,!0,!0,!0,!0);t.keyboardOnlyEmoji=a=>u(a,!1,!1,!1,!0,!0,!0,!1,!1,!1,!0);t.keyboardOnly=a=>u(a,!0,!0,!0,!0,!0,!0,!0,!1,!0,!0);
|
|
4
|
+
function u(a,c=!1,d=!1,f=!1,h=!1,g=!1,l=!1,b=!1,e=!1,k=!1,r=!1,m=Infinity,p="throw"){if("string"!==typeof a)throw new TypeError("sanctifyText expects a string input.");"truncate"!==p&&"noop"!==p&&"throw"!==p&&(p="throw");if(a.length>m)switch(p){case "truncate":a=a.slice(0,m);break;case "noop":return a;default:throw new RangeError(`sanctifyText input length ${a.length} exceeds maxLength ${m}.`);}c&&(a=a.replace(q,""));d&&(a=a.replace(v,""));f&&(a=a.replace(n,""));h&&(a=w(a,d));g&&(a=a.replace(x,"\n"));
|
|
5
|
+
l&&(a=a.replace(y,"$1"));b&&(c=a,a=e?c.replace(z,"\n\n"):c.replace(B,"\n"));k&&(a=a.replace(C," "));return r?a.trim():a}var D=/[^\x20-\x7E\r\n]+/gu;function w(a,c=!1){a=E(a);return c?a.replace(D,""):a.replace(D,d=>{v.lastIndex=0;if(v.test(d)){v.lastIndex=0;var f="";for(const h of d.matchAll(v))f+=h[0];d=f}else d="";return d})}
|
|
6
|
+
var F=/[\u2018\u2019\u201A\u201B\u2032\u2035]/g,G=/[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g,H=/[\u2012\u2013\u2014\u2015\u2212]/g,I=/\u2026/g,J=/[\u2022\u00B7]/g,K=/[\uFF01-\uFF5E]/g;function E(a){return a.replace(F,"'").replace(G,'"').replace(H,"-").replace(I,"...").replace(J,"*").replace(K,c=>String.fromCharCode(c.charCodeAt(0)-65248))}var v;try{v=RegExp("(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)","gu")}catch{v=/[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu}
|
|
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 };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category of one BMP code point (one UTF-16 code unit), or null when it is not trash.
|
|
3
|
+
* Surrogates return null; the caller pairs them and uses classifyAstral.
|
|
4
|
+
* @param {number} c
|
|
5
|
+
* @returns {TrashCategory|null}
|
|
6
|
+
*/
|
|
7
|
+
export function classifyCodeUnit(c: number): TrashCategory | null;
|
|
8
|
+
/**
|
|
9
|
+
* Category of a code point above U+FFFF, or null when it is not trash.
|
|
10
|
+
* @param {number} c
|
|
11
|
+
* @returns {TrashCategory|null}
|
|
12
|
+
*/
|
|
13
|
+
export function classifyAstral(c: number): TrashCategory | null;
|
|
14
|
+
/** @typedef {import('./inspectText.js').TrashCategory} TrashCategory */
|
|
15
|
+
/**
|
|
16
|
+
* Removed by `nukeControls`. Categories: bidi_control, nul, c0_control, c1_control.
|
|
17
|
+
* Global: reset `lastIndex` before calling `.test()`.
|
|
18
|
+
* @type {RegExp}
|
|
19
|
+
*/
|
|
20
|
+
export const CONTROL_CHARS_REGEX: RegExp;
|
|
21
|
+
/**
|
|
22
|
+
* Removed by `purgeInvisibleChars`. Categories: bidi_control, zero_width, nonbreaking_space, bom, invisible_format, hangul_filler, variation_selector, tag.
|
|
23
|
+
* Global: reset `lastIndex` before calling `.test()`.
|
|
24
|
+
* @type {RegExp}
|
|
25
|
+
*/
|
|
26
|
+
export const INVISIBLE_TRASH_REGEX: RegExp;
|
|
27
|
+
export type TrashCategory = import("./inspectText.js").TrashCategory;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes tag characters to the ASCII they shadow: U+E0020–U+E007E become
|
|
3
|
+
* U+0020–U+007E. Anything else (U+E0001 LANGUAGE TAG, U+E007F CANCEL TAG, unassigned
|
|
4
|
+
* tags, other characters) is written as `<U+XXXX>`, so the result is printable.
|
|
5
|
+
* @param {string} text
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function decodeTagText(text: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Finds runs of tag characters, variation selectors and invisible operators.
|
|
11
|
+
* Consecutive characters of the same kind form one run. Two kinds of sequence are
|
|
12
|
+
* legitimate and listed under `benign` instead of `runs`:
|
|
13
|
+
* - the subdivision flags of England, Scotland and Wales: U+1F3F4 followed by the
|
|
14
|
+
* tags for "gbeng", "gbsct" or "gbwls" and U+E007F
|
|
15
|
+
* - a single U+FE0E or U+FE0F (text or emoji presentation) after another character
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {HiddenTextScan}
|
|
18
|
+
*/
|
|
19
|
+
export function findHiddenRuns(text: string): HiddenTextScan;
|
|
20
|
+
export type HiddenRunKind = "tag" | "variation_selector" | "invisible_operator";
|
|
21
|
+
export type HiddenRun = {
|
|
22
|
+
kind: HiddenRunKind;
|
|
23
|
+
/**
|
|
24
|
+
* - UTF-16 index of the first character
|
|
25
|
+
*/
|
|
26
|
+
offset: number;
|
|
27
|
+
/**
|
|
28
|
+
* - UTF-16 index just past the last character
|
|
29
|
+
*/
|
|
30
|
+
end: number;
|
|
31
|
+
/**
|
|
32
|
+
* - number of code points in the run
|
|
33
|
+
*/
|
|
34
|
+
count: number;
|
|
35
|
+
/**
|
|
36
|
+
* - tag runs only: the ASCII the tags spell (see decodeTagText)
|
|
37
|
+
*/
|
|
38
|
+
decoded?: string;
|
|
39
|
+
};
|
|
40
|
+
export type HiddenTextScan = {
|
|
41
|
+
/**
|
|
42
|
+
* - runs that may hide text, in order
|
|
43
|
+
*/
|
|
44
|
+
runs: HiddenRun[];
|
|
45
|
+
/**
|
|
46
|
+
* - [offset, end) spans of legitimate
|
|
47
|
+
* sequences (subdivision flags, a single emoji or text presentation selector)
|
|
48
|
+
*/
|
|
49
|
+
benign: Array<[number, number]>;
|
|
50
|
+
};
|
|
@@ -6,18 +6,18 @@ export function inspectText(text: string): UnicodeTrashReport;
|
|
|
6
6
|
/**
|
|
7
7
|
* Locates invisible and control characters, one finding per character.
|
|
8
8
|
*
|
|
9
|
-
* Categories:
|
|
9
|
+
* Categories (generated from scripts/categories.js by `npm run gen`):
|
|
10
10
|
* - `bidi_control`: U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069
|
|
11
11
|
* - `zero_width`: U+200B–U+200D, U+2060
|
|
12
|
-
* - `nonbreaking_space`:
|
|
13
|
-
* - `bom`: U+FEFF (`atStart`
|
|
12
|
+
* - `nonbreaking_space`: U+00A0, U+2000–U+200A, U+202F, U+205F, U+3000 (non-breaking and typographic spaces)
|
|
13
|
+
* - `bom`: U+FEFF (`atStart` is true at offset 0, where a BOM is legitimate)
|
|
14
14
|
* - `nul`: U+0000
|
|
15
|
-
* - `c0_control`: U+0001–U+001F
|
|
15
|
+
* - `c0_control`: U+0001–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F (tab, LF and CR are whitespace, not trash)
|
|
16
16
|
* - `c1_control`: U+0080–U+009F
|
|
17
|
-
* - `invisible_format`: soft hyphen U+
|
|
17
|
+
* - `invisible_format`: U+00AD (soft hyphen), U+034F (combining grapheme joiner), U+17B4, U+17B5 (Khmer inherent vowels), U+180E (Mongolian vowel separator), U+2061–U+2064 (invisible operators), U+2065 (reserved), U+206A–U+206F (deprecated format characters), U+FFF0–U+FFF8 (reserved), U+1BCA0–U+1BCA3 (shorthand format controls), U+1D173–U+1D17A (musical formatting), U+E0080–U+E00FF (reserved), U+E01F0–U+E0FFF (reserved) (every other Default_Ignorable_Code_Point, which renderers draw as nothing)
|
|
18
18
|
* - `hangul_filler`: U+115F, U+1160, U+3164, U+FFA0 (letters that render blank)
|
|
19
|
-
* - `variation_selector`: U+
|
|
20
|
-
* - `tag`: U+E0000–U+E007F (tag characters, used for "ASCII smuggling")
|
|
19
|
+
* - `variation_selector`: U+180B–U+180D (Mongolian FVS1-FVS3), U+180F (Mongolian FVS4), U+FE00–U+FE0F (VS1-VS16, including VS16 in emoji), U+E0100–U+E01EF (VS17-VS256)
|
|
20
|
+
* - `tag`: U+E0000–U+E007F (tag characters, used for "ASCII smuggling" of hidden instructions)
|
|
21
21
|
*
|
|
22
22
|
* `codePoint` is the full code point; `offset` and `column` count UTF-16 units.
|
|
23
23
|
* Every occurrence is counted; only the first `maxFindings` are listed.
|
|
@@ -117,22 +117,6 @@ export function sanctifyText(text: string, purgeInvisibleChars?: boolean, purgeE
|
|
|
117
117
|
* @returns {string}
|
|
118
118
|
*/
|
|
119
119
|
export function normalizeTypographicJank(text: string): string;
|
|
120
|
-
/**
|
|
121
|
-
* Purges invisible Unicode "trash" characters and removes them.
|
|
122
|
-
*
|
|
123
|
-
* Targets:
|
|
124
|
-
* - Non-breaking spaces ( )
|
|
125
|
-
* - Zero-width spaces and miscellaneous Unicode spaces ( –, , , , , )
|
|
126
|
-
* - Bidi controls: marks, embeddings/overrides and isolates (, , , –, –)
|
|
127
|
-
* - Invisible format characters: soft hyphen, CGJ, Mongolian vowel separator, invisible operators (, ͏, , –)
|
|
128
|
-
* - Hangul fillers (ᅟ, ᅠ, ㅤ, ᅠ)
|
|
129
|
-
* - Variation selectors (︀–️, \u{E0100}–\u{E01EF}), including VS16 in emoji
|
|
130
|
-
* - Tag characters (\u{E0000}–\u{E007F}), used for "ASCII smuggling"
|
|
131
|
-
*
|
|
132
|
-
* @param {string} text
|
|
133
|
-
* @returns {string}
|
|
134
|
-
*/
|
|
135
|
-
export const INVISIBLE_TRASH_REGEX: RegExp;
|
|
136
120
|
/**
|
|
137
121
|
* Matches any character that is NOT:
|
|
138
122
|
* - Printable ASCII (U+0020–U+007E)
|
|
@@ -142,19 +126,6 @@ export const INVISIBLE_TRASH_REGEX: RegExp;
|
|
|
142
126
|
export const ASCII_KEYBOARD_SAFE_REGEX: RegExp;
|
|
143
127
|
/** @type {RegExp} */
|
|
144
128
|
export let EMOJI_REGEX: RegExp;
|
|
145
|
-
/**
|
|
146
|
-
* Nukes hidden control characters that are invisible and often dangerous.
|
|
147
|
-
* (Excludes necessary whitespace like \n and \t.)
|
|
148
|
-
*
|
|
149
|
-
* Control characters nuked:
|
|
150
|
-
* - ASCII control range (0x00-0x1F, 0x7F)
|
|
151
|
-
* - Unicode control range (0x80-0x9F)
|
|
152
|
-
* - Bidi controls (U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069)
|
|
153
|
-
*
|
|
154
|
-
* @param {string} text
|
|
155
|
-
* @returns {string}
|
|
156
|
-
*/
|
|
157
|
-
export const CONTROL_CHARS_REGEX: RegExp;
|
|
158
129
|
export type SanctifyOptions = {
|
|
159
130
|
purgeInvisibleChars?: boolean;
|
|
160
131
|
purgeEmojis?: boolean;
|
|
@@ -169,3 +140,6 @@ export type SanctifyOptions = {
|
|
|
169
140
|
maxLength?: number;
|
|
170
141
|
onMaxLength?: "throw" | "truncate" | "noop";
|
|
171
142
|
};
|
|
143
|
+
import { CONTROL_CHARS_REGEX } from './categories.generated.js';
|
|
144
|
+
import { INVISIBLE_TRASH_REGEX } from './categories.generated.js';
|
|
145
|
+
export { CONTROL_CHARS_REGEX, INVISIBLE_TRASH_REGEX };
|
package/package.json
CHANGED
|
@@ -1,74 +1,80 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "text-sanctifier",
|
|
3
|
-
"version": "1.3.
|
|
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
|
-
"./
|
|
31
|
-
"types": "./dist/types/
|
|
32
|
-
"default": "./
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
}
|
|
74
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "text-sanctifier",
|
|
3
|
+
"version": "1.3.3",
|
|
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
|
+
"./browser": {
|
|
35
|
+
"types": "./dist/types/index.d.ts",
|
|
36
|
+
"default": "./dist/text-sanctifier.min.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"types": "./dist/types/index.d.ts",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"gen": "node scripts/gen-categories.js",
|
|
43
|
+
"gen:check": "node scripts/gen-categories.js --check",
|
|
44
|
+
"build": "node scripts/buildc.js",
|
|
45
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
46
|
+
"test:types": "tsc -p tests/types/tsconfig.json",
|
|
47
|
+
"test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node tests/cli.test.js",
|
|
48
|
+
"test-min": "node tests/sanctifyText.test.min.js",
|
|
49
|
+
"ci": "npm run gen:check && npm run test && npm run build && npm run build:types && npm run test:types && npm run test-min"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"text",
|
|
53
|
+
"sanitize",
|
|
54
|
+
"normalize",
|
|
55
|
+
"invisible",
|
|
56
|
+
"scrubber",
|
|
57
|
+
"unicode",
|
|
58
|
+
"clean",
|
|
59
|
+
"sanctify"
|
|
60
|
+
],
|
|
61
|
+
"author": "WATT3D <WATT3D@protonmail.com>",
|
|
62
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/iWhatty/text-sanctifier.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/iWhatty/text-sanctifier/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/iWhatty/text-sanctifier#readme",
|
|
71
|
+
"directories": {
|
|
72
|
+
"test": "tests"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"esbuild": "^0.25.3",
|
|
76
|
+
"google-closure-compiler": "^20240317.0.0",
|
|
77
|
+
"terser": "^5.39.0",
|
|
78
|
+
"typescript": "^5.9.3"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/categories.generated.js
|
|
2
|
+
//
|
|
3
|
+
// Generated by scripts/gen-categories.js from the table in scripts/categories.js.
|
|
4
|
+
// Do not edit: change the table and run `npm run gen`. `npm run ci` fails while this file is stale.
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./inspectText.js').TrashCategory} TrashCategory */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Removed by `nukeControls`. Categories: bidi_control, nul, c0_control, c1_control.
|
|
10
|
+
* Global: reset `lastIndex` before calling `.test()`.
|
|
11
|
+
* @type {RegExp}
|
|
12
|
+
*/
|
|
13
|
+
export const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Removed by `purgeInvisibleChars`. Categories: bidi_control, zero_width, nonbreaking_space, bom, invisible_format, hangul_filler, variation_selector, tag.
|
|
17
|
+
* Global: reset `lastIndex` before calling `.test()`.
|
|
18
|
+
* @type {RegExp}
|
|
19
|
+
*/
|
|
20
|
+
export const INVISIBLE_TRASH_REGEX = /[\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;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Category of one BMP code point (one UTF-16 code unit), or null when it is not trash.
|
|
24
|
+
* Surrogates return null; the caller pairs them and uses classifyAstral.
|
|
25
|
+
* @param {number} c
|
|
26
|
+
* @returns {TrashCategory|null}
|
|
27
|
+
*/
|
|
28
|
+
export function classifyCodeUnit(c) {
|
|
29
|
+
return c < 0x2000 ? classifyBelow2000(c) : classifyFrom2000(c);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Two halves rather than one tree: V8 inlines a function only while its bytecode is
|
|
33
|
+
// small (460 bytes by default), and the single pass is much faster when it does.
|
|
34
|
+
|
|
35
|
+
/** @param {number} c @returns {TrashCategory|null} */
|
|
36
|
+
function classifyBelow2000(c) {
|
|
37
|
+
if (c < 0x00AE) {
|
|
38
|
+
if (c < 0x0020) {
|
|
39
|
+
if (c < 0x000B) {
|
|
40
|
+
if (c < 0x0001) {
|
|
41
|
+
return 'nul';
|
|
42
|
+
}
|
|
43
|
+
if (c < 0x0009) {
|
|
44
|
+
return 'c0_control';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (c < 0x000D) {
|
|
49
|
+
return 'c0_control';
|
|
50
|
+
}
|
|
51
|
+
if (c < 0x000E) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return 'c0_control';
|
|
55
|
+
}
|
|
56
|
+
if (c < 0x00A0) {
|
|
57
|
+
if (c < 0x007F) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (c < 0x0080) {
|
|
61
|
+
return 'c0_control';
|
|
62
|
+
}
|
|
63
|
+
return 'c1_control';
|
|
64
|
+
}
|
|
65
|
+
if (c < 0x00A1) {
|
|
66
|
+
return 'nonbreaking_space';
|
|
67
|
+
}
|
|
68
|
+
if (c < 0x00AD) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return 'invisible_format';
|
|
72
|
+
}
|
|
73
|
+
if (c < 0x1161) {
|
|
74
|
+
if (c < 0x061C) {
|
|
75
|
+
if (c < 0x034F) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (c < 0x0350) {
|
|
79
|
+
return 'invisible_format';
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (c < 0x061D) {
|
|
84
|
+
return 'bidi_control';
|
|
85
|
+
}
|
|
86
|
+
if (c < 0x115F) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return 'hangul_filler';
|
|
90
|
+
}
|
|
91
|
+
if (c < 0x180B) {
|
|
92
|
+
if (c < 0x17B4) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (c < 0x17B6) {
|
|
96
|
+
return 'invisible_format';
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (c < 0x180F) {
|
|
101
|
+
if (c < 0x180E) {
|
|
102
|
+
return 'variation_selector';
|
|
103
|
+
}
|
|
104
|
+
return 'invisible_format';
|
|
105
|
+
}
|
|
106
|
+
if (c < 0x1810) {
|
|
107
|
+
return 'variation_selector';
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {number} c @returns {TrashCategory|null} */
|
|
113
|
+
function classifyFrom2000(c) {
|
|
114
|
+
if (c < 0x2070) {
|
|
115
|
+
if (c < 0x2030) {
|
|
116
|
+
if (c < 0x2010) {
|
|
117
|
+
if (c < 0x200B) {
|
|
118
|
+
return 'nonbreaking_space';
|
|
119
|
+
}
|
|
120
|
+
if (c < 0x200E) {
|
|
121
|
+
return 'zero_width';
|
|
122
|
+
}
|
|
123
|
+
return 'bidi_control';
|
|
124
|
+
}
|
|
125
|
+
if (c < 0x202A) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (c < 0x202F) {
|
|
129
|
+
return 'bidi_control';
|
|
130
|
+
}
|
|
131
|
+
return 'nonbreaking_space';
|
|
132
|
+
}
|
|
133
|
+
if (c < 0x2061) {
|
|
134
|
+
if (c < 0x205F) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (c < 0x2060) {
|
|
138
|
+
return 'nonbreaking_space';
|
|
139
|
+
}
|
|
140
|
+
return 'zero_width';
|
|
141
|
+
}
|
|
142
|
+
if (c < 0x2066) {
|
|
143
|
+
return 'invisible_format';
|
|
144
|
+
}
|
|
145
|
+
if (c < 0x206A) {
|
|
146
|
+
return 'bidi_control';
|
|
147
|
+
}
|
|
148
|
+
return 'invisible_format';
|
|
149
|
+
}
|
|
150
|
+
if (c < 0xFE10) {
|
|
151
|
+
if (c < 0x3164) {
|
|
152
|
+
if (c < 0x3000) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (c < 0x3001) {
|
|
156
|
+
return 'nonbreaking_space';
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (c < 0x3165) {
|
|
161
|
+
return 'hangul_filler';
|
|
162
|
+
}
|
|
163
|
+
if (c < 0xFE00) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return 'variation_selector';
|
|
167
|
+
}
|
|
168
|
+
if (c < 0xFFA0) {
|
|
169
|
+
if (c < 0xFEFF) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (c < 0xFF00) {
|
|
173
|
+
return 'bom';
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (c < 0xFFF0) {
|
|
178
|
+
if (c < 0xFFA1) {
|
|
179
|
+
return 'hangul_filler';
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (c < 0xFFF9) {
|
|
184
|
+
return 'invisible_format';
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Category of a code point above U+FFFF, or null when it is not trash.
|
|
191
|
+
* @param {number} c
|
|
192
|
+
* @returns {TrashCategory|null}
|
|
193
|
+
*/
|
|
194
|
+
export function classifyAstral(c) {
|
|
195
|
+
if (c < 0xE0000) {
|
|
196
|
+
if (c < 0x1BCA4) {
|
|
197
|
+
if (c < 0x1BCA0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return 'invisible_format';
|
|
201
|
+
}
|
|
202
|
+
if (c < 0x1D173) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (c < 0x1D17B) {
|
|
206
|
+
return 'invisible_format';
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (c < 0xE0100) {
|
|
211
|
+
if (c < 0xE0080) {
|
|
212
|
+
return 'tag';
|
|
213
|
+
}
|
|
214
|
+
return 'invisible_format';
|
|
215
|
+
}
|
|
216
|
+
if (c < 0xE01F0) {
|
|
217
|
+
return 'variation_selector';
|
|
218
|
+
}
|
|
219
|
+
if (c < 0xE1000) {
|
|
220
|
+
return 'invisible_format';
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|