text-sanctifier 1.3.6 → 1.3.8
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 +437 -411
- package/dist/text-sanctifier.min.js +1 -1
- package/dist/types/context.d.ts +66 -0
- package/dist/types/hiddenText.d.ts +9 -0
- package/dist/types/version.generated.d.ts +1 -1
- package/dist/types/virama.generated.d.ts +1 -0
- package/package.json +83 -79
- package/src/context.js +217 -0
- package/src/hiddenText.js +11 -1
- package/src/version.generated.js +5 -5
- package/src/virama.generated.js +5 -0
|
@@ -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.
|
|
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};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates invisible and control characters as inspectTextDetailed does, and marks
|
|
3
|
+
* the ones that belong to legitimate sequences as benign:
|
|
4
|
+
* - U+200D between two emoji (Extended_Pictographic), allowing a skin-tone modifier or
|
|
5
|
+
* VS16 after the first: an emoji ZWJ sequence
|
|
6
|
+
* - U+200C or U+200D right after a virama (Devanagari, Bengali, Malayalam, Sinhala and
|
|
7
|
+
* the other scripts with a combining class 9 mark), as IDNA ContextJ allows
|
|
8
|
+
* - U+200C between two Arabic-script letters, nonspacing marks skipped (Persian and
|
|
9
|
+
* others); an approximation of IDNA's joining-type rule
|
|
10
|
+
* - U+FE0E or U+FE0F directly after an emoji, or between 0-9, # or * and U+20E3 (keycap)
|
|
11
|
+
* - one of U+E0100–U+E01EF directly after an ideograph: an ideographic variation sequence
|
|
12
|
+
* - U+180B–U+180D or U+180F directly after a Mongolian letter
|
|
13
|
+
* - the tags of the England, Scotland and Wales flags after U+1F3F4
|
|
14
|
+
* Nothing else is benign: not U+200B, U+2060, U+FEFF, bidi controls or other tags.
|
|
15
|
+
*
|
|
16
|
+
* `counts`, `total` and `truncated` are exactly those of inspectTextDetailed;
|
|
17
|
+
* `benignCounts` counts every benign character, listed or not. Two passes over the
|
|
18
|
+
* string, no copy; memory is flat with `maxFindings` set.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} text
|
|
21
|
+
* @param {InspectDetailedOptions} [options]
|
|
22
|
+
* @returns {ContextReport}
|
|
23
|
+
*/
|
|
24
|
+
export function inspectTextInContext(text: string, options?: InspectDetailedOptions): ContextReport;
|
|
25
|
+
export type TrashFinding = import("./inspectText.js").TrashFinding;
|
|
26
|
+
export type TrashCounts = import("./inspectText.js").TrashCounts;
|
|
27
|
+
export type InspectDetailedOptions = import("./inspectText.js").InspectDetailedOptions;
|
|
28
|
+
/**
|
|
29
|
+
* A finding with `benign`: true when the character is part of a legitimate sequence.
|
|
30
|
+
*/
|
|
31
|
+
export type ContextFinding = TrashFinding & {
|
|
32
|
+
benign: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type ContextReport = {
|
|
35
|
+
/**
|
|
36
|
+
* - As in inspectTextDetailed, each with `benign`.
|
|
37
|
+
*/
|
|
38
|
+
findings: ContextFinding[];
|
|
39
|
+
/**
|
|
40
|
+
* - As in inspectTextDetailed: every occurrence, benign or not.
|
|
41
|
+
*/
|
|
42
|
+
counts: TrashCounts;
|
|
43
|
+
/**
|
|
44
|
+
* - Sum of `counts`.
|
|
45
|
+
*/
|
|
46
|
+
total: number;
|
|
47
|
+
/**
|
|
48
|
+
* - True when `total` exceeds `findings.length`.
|
|
49
|
+
*/
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* - Benign occurrences per category, listed or not.
|
|
53
|
+
* `counts[c] - benignCounts[c]` is the number of suspicious characters in category `c`.
|
|
54
|
+
*/
|
|
55
|
+
benignCounts: TrashCounts;
|
|
56
|
+
/**
|
|
57
|
+
* - Sum of `benignCounts`.
|
|
58
|
+
*/
|
|
59
|
+
benignTotal: number;
|
|
60
|
+
/**
|
|
61
|
+
* - Benign U+E0100–U+E01EF (VS17–VS256),
|
|
62
|
+
* part of `benignCounts.variation_selector`. `supplementarySelectorCount` from findHiddenRuns
|
|
63
|
+
* minus this is the number of suspicious ones.
|
|
64
|
+
*/
|
|
65
|
+
benignSupplementarySelectorCount: number;
|
|
66
|
+
};
|
|
@@ -79,6 +79,15 @@ export type HiddenTextScan = {
|
|
|
79
79
|
* - every run per kind, listed or not
|
|
80
80
|
*/
|
|
81
81
|
counts: HiddenRunCounts;
|
|
82
|
+
/**
|
|
83
|
+
* - code points in those runs per kind, listed or not
|
|
84
|
+
*/
|
|
85
|
+
codePointCounts: HiddenRunCounts;
|
|
86
|
+
/**
|
|
87
|
+
* - code points U+E0100–U+E01EF in those runs, which
|
|
88
|
+
* (unlike the emoji selectors U+FE00–U+FE0F) can carry data; included in codePointCounts.variation_selector
|
|
89
|
+
*/
|
|
90
|
+
supplementarySelectorCount: number;
|
|
82
91
|
/**
|
|
83
92
|
* - sum of `counts`
|
|
84
93
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The installed version of text-sanctifier, as in its package.json. */
|
|
2
|
-
export const VERSION: "1.3.
|
|
2
|
+
export const VERSION: "1.3.8";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VIRAMA_AT: RegExp;
|
package/package.json
CHANGED
|
@@ -1,79 +1,83 @@
|
|
|
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
|
-
"./hidden": {
|
|
31
|
-
"types": "./dist/types/hiddenText.d.ts",
|
|
32
|
-
"default": "./src/hiddenText.js"
|
|
33
|
-
},
|
|
34
|
-
"./
|
|
35
|
-
"types": "./dist/types/
|
|
36
|
-
"default": "./
|
|
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
|
-
},
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
}
|
|
79
|
-
|
|
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
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// src/context.js
|
|
2
|
+
//
|
|
3
|
+
// inspectTextInContext: inspectTextDetailed plus which findings belong to legitimate
|
|
4
|
+
// sequences (emoji joiners and selectors, joiners in Indic and Arabic-script text,
|
|
5
|
+
// ideographic variation sequences, Mongolian selectors, subdivision flags), counted
|
|
6
|
+
// exactly per category. The categories are
|
|
7
|
+
// unchanged; see "Proposal: context-aware findings" in DESIGN.md.
|
|
8
|
+
//
|
|
9
|
+
// Exported as `text-sanctifier/context`. It needs Unicode property escapes in regexes,
|
|
10
|
+
// so it lives apart from the core and the browser file and has its own size budget.
|
|
11
|
+
|
|
12
|
+
import { inspectTextDetailed } from './inspectText.js';
|
|
13
|
+
import { VIRAMA_AT } from './virama.generated.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('./inspectText.js').TrashFinding} TrashFinding */
|
|
16
|
+
/** @typedef {import('./inspectText.js').TrashCounts} TrashCounts */
|
|
17
|
+
/** @typedef {import('./inspectText.js').InspectDetailedOptions} InspectDetailedOptions */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {TrashFinding & { benign: boolean }} ContextFinding
|
|
21
|
+
* A finding with `benign`: true when the character is part of a legitimate sequence.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} ContextReport
|
|
26
|
+
* @property {ContextFinding[]} findings - As in inspectTextDetailed, each with `benign`.
|
|
27
|
+
* @property {TrashCounts} counts - As in inspectTextDetailed: every occurrence, benign or not.
|
|
28
|
+
* @property {number} total - Sum of `counts`.
|
|
29
|
+
* @property {boolean} truncated - True when `total` exceeds `findings.length`.
|
|
30
|
+
* @property {TrashCounts} benignCounts - Benign occurrences per category, listed or not.
|
|
31
|
+
* `counts[c] - benignCounts[c]` is the number of suspicious characters in category `c`.
|
|
32
|
+
* @property {number} benignTotal - Sum of `benignCounts`.
|
|
33
|
+
* @property {number} benignSupplementarySelectorCount - Benign U+E0100–U+E01EF (VS17–VS256),
|
|
34
|
+
* part of `benignCounts.variation_selector`. `supplementarySelectorCount` from findHiddenRuns
|
|
35
|
+
* minus this is the number of suspicious ones.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Tested at an index of the text itself, so nothing is allocated per character.
|
|
39
|
+
const PICTOGRAPHIC_AT = /\p{Extended_Pictographic}/yu;
|
|
40
|
+
const IDEOGRAPHIC_AT = /\p{Ideographic}/yu;
|
|
41
|
+
const ARABIC_LETTER_AT = /(?=\p{Script=Arabic})\p{L}/yu;
|
|
42
|
+
const MARK_AT = /\p{Mn}/yu;
|
|
43
|
+
|
|
44
|
+
// RGI subdivision flags: U+1F3F4, the tag letters, U+E007F
|
|
45
|
+
const SUBDIVISION_FLAGS = ['gbeng', 'gbsct', 'gbwls'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {RegExp} re - sticky
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @param {number} index
|
|
51
|
+
*/
|
|
52
|
+
function matchesAt(re, text, index) {
|
|
53
|
+
re.lastIndex = index;
|
|
54
|
+
return re.test(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Index where the code point that ends just before `index` starts, or -1.
|
|
59
|
+
* @param {string} text
|
|
60
|
+
* @param {number} index
|
|
61
|
+
*/
|
|
62
|
+
function previousStart(text, index) {
|
|
63
|
+
if (index <= 0) return -1;
|
|
64
|
+
const low = text.charCodeAt(index - 1);
|
|
65
|
+
if (low >= 0xDC00 && low <= 0xDFFF && index >= 2) {
|
|
66
|
+
const high = text.charCodeAt(index - 2);
|
|
67
|
+
if (high >= 0xD800 && high <= 0xDBFF) return index - 2;
|
|
68
|
+
}
|
|
69
|
+
return index - 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {number} c */
|
|
73
|
+
const isMongolianLetter = (c) =>
|
|
74
|
+
(c >= 0x1820 && c <= 0x1878) || (c >= 0x1880 && c <= 0x1884) || (c >= 0x1887 && c <= 0x18A8) || c === 0x18AA;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A U+200D is benign between two Extended_Pictographic code points, allowing a
|
|
78
|
+
* skin-tone modifier or VS16 after the first.
|
|
79
|
+
* @param {string} text
|
|
80
|
+
* @param {number} i - index of the U+200D
|
|
81
|
+
*/
|
|
82
|
+
function isEmojiJoiner(text, i) {
|
|
83
|
+
let p = previousStart(text, i);
|
|
84
|
+
if (p < 0) return false;
|
|
85
|
+
const before = /** @type {number} */ (text.codePointAt(p));
|
|
86
|
+
if (before === 0xFE0F || (before >= 0x1F3FB && before <= 0x1F3FF)) p = previousStart(text, p);
|
|
87
|
+
return p >= 0 && matchesAt(PICTOGRAPHIC_AT, text, p) && matchesAt(PICTOGRAPHIC_AT, text, i + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* IDNA ContextJ (RFC 5892, appendix A.1 and A.2): a U+200C or U+200D is benign right
|
|
92
|
+
* after a virama, and a U+200C also between two Arabic-script letters, skipping
|
|
93
|
+
* nonspacing marks on either side. The second rule approximates the RFC's joining types
|
|
94
|
+
* (L or D before, R or D after), which JavaScript regexes can't express: it also allows
|
|
95
|
+
* a ZWNJ after a right-joining or non-joining Arabic letter, and none in other joining
|
|
96
|
+
* scripts such as Syriac or N'Ko.
|
|
97
|
+
* @param {string} text
|
|
98
|
+
* @param {number} i - index of the U+200C or U+200D
|
|
99
|
+
* @param {boolean} zwnj - true for U+200C
|
|
100
|
+
*/
|
|
101
|
+
function isScriptJoiner(text, i, zwnj) {
|
|
102
|
+
let p = previousStart(text, i);
|
|
103
|
+
if (p < 0) return false;
|
|
104
|
+
if (matchesAt(VIRAMA_AT, text, p)) return true;
|
|
105
|
+
if (!zwnj) return false;
|
|
106
|
+
while (p >= 0 && matchesAt(MARK_AT, text, p)) p = previousStart(text, p);
|
|
107
|
+
if (p < 0 || !matchesAt(ARABIC_LETTER_AT, text, p)) return false;
|
|
108
|
+
let q = i + 1;
|
|
109
|
+
while (matchesAt(MARK_AT, text, q)) q = MARK_AT.lastIndex;
|
|
110
|
+
return matchesAt(ARABIC_LETTER_AT, text, q);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Length in UTF-16 units of the subdivision-flag tags starting at `i` (just after a
|
|
115
|
+
* U+1F3F4), or 0 when they are not exactly one of the RGI flags, as findHiddenRuns decides.
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {number} i
|
|
118
|
+
*/
|
|
119
|
+
function flagTagsAt(text, i) {
|
|
120
|
+
// five tag letters and CANCEL TAG, each a surrogate pair
|
|
121
|
+
if (text.charCodeAt(i + 10) !== 0xDB40 || text.charCodeAt(i + 11) !== 0xDC7F) return 0;
|
|
122
|
+
// more tags after CANCEL TAG make it one longer tag run, which is not a flag
|
|
123
|
+
const after = text.charCodeAt(i + 13);
|
|
124
|
+
if (text.charCodeAt(i + 12) === 0xDB40 && after >= 0xDC00 && after <= 0xDC7F) return 0;
|
|
125
|
+
let letters = '';
|
|
126
|
+
for (let k = 0; k < 10; k += 2) {
|
|
127
|
+
const low = text.charCodeAt(i + k + 1);
|
|
128
|
+
if (text.charCodeAt(i + k) !== 0xDB40 || low < 0xDC61 || low > 0xDC7A) return 0; // tag a-z
|
|
129
|
+
letters += String.fromCharCode(low - 0xDC00);
|
|
130
|
+
}
|
|
131
|
+
return SUBDIVISION_FLAGS.includes(letters) ? 12 : 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Locates invisible and control characters as inspectTextDetailed does, and marks
|
|
136
|
+
* the ones that belong to legitimate sequences as benign:
|
|
137
|
+
* - U+200D between two emoji (Extended_Pictographic), allowing a skin-tone modifier or
|
|
138
|
+
* VS16 after the first: an emoji ZWJ sequence
|
|
139
|
+
* - U+200C or U+200D right after a virama (Devanagari, Bengali, Malayalam, Sinhala and
|
|
140
|
+
* the other scripts with a combining class 9 mark), as IDNA ContextJ allows
|
|
141
|
+
* - U+200C between two Arabic-script letters, nonspacing marks skipped (Persian and
|
|
142
|
+
* others); an approximation of IDNA's joining-type rule
|
|
143
|
+
* - U+FE0E or U+FE0F directly after an emoji, or between 0-9, # or * and U+20E3 (keycap)
|
|
144
|
+
* - one of U+E0100–U+E01EF directly after an ideograph: an ideographic variation sequence
|
|
145
|
+
* - U+180B–U+180D or U+180F directly after a Mongolian letter
|
|
146
|
+
* - the tags of the England, Scotland and Wales flags after U+1F3F4
|
|
147
|
+
* Nothing else is benign: not U+200B, U+2060, U+FEFF, bidi controls or other tags.
|
|
148
|
+
*
|
|
149
|
+
* `counts`, `total` and `truncated` are exactly those of inspectTextDetailed;
|
|
150
|
+
* `benignCounts` counts every benign character, listed or not. Two passes over the
|
|
151
|
+
* string, no copy; memory is flat with `maxFindings` set.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} text
|
|
154
|
+
* @param {InspectDetailedOptions} [options]
|
|
155
|
+
* @returns {ContextReport}
|
|
156
|
+
*/
|
|
157
|
+
export function inspectTextInContext(text, options = {}) {
|
|
158
|
+
if (typeof text !== 'string') {
|
|
159
|
+
throw new TypeError('inspectTextInContext expects a string input.');
|
|
160
|
+
}
|
|
161
|
+
const report = inspectTextDetailed(text, options);
|
|
162
|
+
const findings = /** @type {ContextFinding[]} */ (report.findings);
|
|
163
|
+
for (const f of findings) f.benign = false;
|
|
164
|
+
|
|
165
|
+
/** @type {TrashCounts} */
|
|
166
|
+
const benignCounts = /** @type {TrashCounts} */ ({});
|
|
167
|
+
for (const key of Object.keys(report.counts)) benignCounts[/** @type {keyof TrashCounts} */ (key)] = 0;
|
|
168
|
+
let benignTotal = 0;
|
|
169
|
+
let benignSupplementarySelectorCount = 0;
|
|
170
|
+
let next = 0; // first listed finding not yet passed
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {number} offset
|
|
174
|
+
* @param {keyof TrashCounts} category
|
|
175
|
+
*/
|
|
176
|
+
const markBenign = (offset, category) => {
|
|
177
|
+
benignCounts[category]++;
|
|
178
|
+
benignTotal++;
|
|
179
|
+
while (next < findings.length && findings[next].offset < offset) next++;
|
|
180
|
+
if (next < findings.length && findings[next].offset === offset) findings[next].benign = true;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
for (let i = 0, n = text.length; i < n; i++) {
|
|
184
|
+
const c = text.charCodeAt(i);
|
|
185
|
+
if (c < 0x180B) continue; // the common case: nothing below can be benign
|
|
186
|
+
|
|
187
|
+
if (c === 0x200D) {
|
|
188
|
+
if (isEmojiJoiner(text, i) || isScriptJoiner(text, i, false)) markBenign(i, 'zero_width');
|
|
189
|
+
} else if (c === 0x200C) {
|
|
190
|
+
if (isScriptJoiner(text, i, true)) markBenign(i, 'zero_width');
|
|
191
|
+
} else if (c === 0xFE0E || c === 0xFE0F) {
|
|
192
|
+
const p = previousStart(text, i);
|
|
193
|
+
const before = p < 0 ? -1 : text.charCodeAt(p);
|
|
194
|
+
const keycap = (before === 0x23 || before === 0x2A || (before >= 0x30 && before <= 0x39)) &&
|
|
195
|
+
text.charCodeAt(i + 1) === 0x20E3;
|
|
196
|
+
if (keycap || (p >= 0 && matchesAt(PICTOGRAPHIC_AT, text, p))) markBenign(i, 'variation_selector');
|
|
197
|
+
} else if ((c >= 0x180B && c <= 0x180D) || c === 0x180F) {
|
|
198
|
+
if (isMongolianLetter(text.charCodeAt(i - 1))) markBenign(i, 'variation_selector');
|
|
199
|
+
} else if (c === 0xD83C && text.charCodeAt(i + 1) === 0xDFF4) { // U+1F3F4
|
|
200
|
+
const length = flagTagsAt(text, i + 2);
|
|
201
|
+
for (let k = 0; k < length; k += 2) markBenign(i + 2 + k, 'tag');
|
|
202
|
+
i += 1 + length;
|
|
203
|
+
} else if (c === 0xDB40) { // plane 14: U+E0000-U+E03FF
|
|
204
|
+
const low = text.charCodeAt(i + 1);
|
|
205
|
+
if (low >= 0xDD00 && low <= 0xDDEF) { // U+E0100-U+E01EF
|
|
206
|
+
const p = previousStart(text, i);
|
|
207
|
+
if (p >= 0 && matchesAt(IDEOGRAPHIC_AT, text, p)) {
|
|
208
|
+
markBenign(i, 'variation_selector');
|
|
209
|
+
benignSupplementarySelectorCount++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (low >= 0xDC00 && low <= 0xDFFF) i++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { findings, counts: report.counts, total: report.total, truncated: report.truncated, benignCounts, benignTotal, benignSupplementarySelectorCount };
|
|
217
|
+
}
|
package/src/hiddenText.js
CHANGED
|
@@ -40,6 +40,9 @@ export { VERSION } from './version.generated.js';
|
|
|
40
40
|
* @property {Array<[number, number]>} benign - [offset, end) spans of legitimate
|
|
41
41
|
* sequences (subdivision flags, a single emoji or text presentation selector), at most `maxRuns` long
|
|
42
42
|
* @property {HiddenRunCounts} counts - every run per kind, listed or not
|
|
43
|
+
* @property {HiddenRunCounts} codePointCounts - code points in those runs per kind, listed or not
|
|
44
|
+
* @property {number} supplementarySelectorCount - code points U+E0100–U+E01EF in those runs, which
|
|
45
|
+
* (unlike the emoji selectors U+FE00–U+FE0F) can carry data; included in codePointCounts.variation_selector
|
|
43
46
|
* @property {number} total - sum of `counts`
|
|
44
47
|
* @property {number} benignTotal - every benign span, listed or not
|
|
45
48
|
* @property {boolean} truncated - true when runs or benign spans were left out of the lists
|
|
@@ -164,6 +167,9 @@ export function findHiddenRuns(text, options = {}) {
|
|
|
164
167
|
const benign = [];
|
|
165
168
|
/** @type {HiddenRunCounts} */
|
|
166
169
|
const counts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
|
|
170
|
+
/** @type {HiddenRunCounts} */
|
|
171
|
+
const codePointCounts = { tag: 0, variation_selector: 0, invisible_operator: 0, reserved_tag_plane: 0 };
|
|
172
|
+
let supplementarySelectorCount = 0;
|
|
167
173
|
let total = 0;
|
|
168
174
|
let benignTotal = 0;
|
|
169
175
|
let line = 1;
|
|
@@ -184,12 +190,14 @@ export function findHiddenRuns(text, options = {}) {
|
|
|
184
190
|
|
|
185
191
|
const offset = i;
|
|
186
192
|
let count = 0;
|
|
193
|
+
let supplementary = 0;
|
|
187
194
|
let last = cp;
|
|
188
195
|
while (i < text.length) {
|
|
189
196
|
const c = /** @type {number} */ (text.codePointAt(i));
|
|
190
197
|
if (kindOf(c) !== kind) break;
|
|
191
198
|
last = c;
|
|
192
199
|
count++;
|
|
200
|
+
if (c >= 0xE0100 && c <= 0xE01EF) supplementary++;
|
|
193
201
|
i += c > 0xFFFF ? 2 : 1;
|
|
194
202
|
}
|
|
195
203
|
const end = i;
|
|
@@ -207,6 +215,8 @@ export function findHiddenRuns(text, options = {}) {
|
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
counts[kind]++;
|
|
218
|
+
codePointCounts[kind] += count;
|
|
219
|
+
supplementarySelectorCount += supplementary;
|
|
210
220
|
total++;
|
|
211
221
|
if (runs.length < maxRuns) {
|
|
212
222
|
/** @type {HiddenRun} */
|
|
@@ -219,5 +229,5 @@ export function findHiddenRuns(text, options = {}) {
|
|
|
219
229
|
runs.push(run);
|
|
220
230
|
}
|
|
221
231
|
}
|
|
222
|
-
return { runs, benign, counts, total, benignTotal, truncated: total > runs.length || benignTotal > benign.length };
|
|
232
|
+
return { runs, benign, counts, codePointCounts, supplementarySelectorCount, total, benignTotal, truncated: total > runs.length || benignTotal > benign.length };
|
|
223
233
|
}
|
package/src/version.generated.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// src/version.generated.js
|
|
2
|
-
// Generated from package.json by scripts/gen-version.js (`npm run gen`); do not edit.
|
|
3
|
-
|
|
4
|
-
/** The installed version of text-sanctifier, as in its package.json. */
|
|
5
|
-
export const VERSION = '1.3.
|
|
1
|
+
// src/version.generated.js
|
|
2
|
+
// Generated from package.json by scripts/gen-version.js (`npm run gen`); do not edit.
|
|
3
|
+
|
|
4
|
+
/** The installed version of text-sanctifier, as in its package.json. */
|
|
5
|
+
export const VERSION = '1.3.8';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// src/virama.generated.js
|
|
2
|
+
// Generated from Node's normalization data by scripts/gen-virama.js (`npm run gen`); do not edit.
|
|
3
|
+
|
|
4
|
+
// The 69 code points with Canonical_Combining_Class 9 (Virama). Sticky: tested at an index.
|
|
5
|
+
export const VIRAMA_AT = /[\u094D\u09CD\u0A4D\u0ACD\u0B4D\u0BCD\u0C4D\u0CCD\u0D3B\u0D3C\u0D4D\u0DCA\u0E3A\u0EBA\u0F84\u1039\u103A\u1714\u1715\u1734\u17D2\u1A60\u1B44\u1BAA\u1BAB\u1BF2\u1BF3\u2D7F\uA806\uA82C\uA8C4\uA953\uA9C0\uAAF6\uABED\u{10A3F}\u{11046}\u{11070}\u{1107F}\u{110B9}\u{11133}\u{11134}\u{111C0}\u{11235}\u{112EA}\u{1134D}\u{113CE}-\u{113D0}\u{11442}\u{114C2}\u{115BF}\u{1163F}\u{116B6}\u{1172B}\u{11839}\u{1193D}\u{1193E}\u{119E0}\u{11A34}\u{11A47}\u{11A99}\u{11C3F}\u{11D44}\u{11D45}\u{11D97}\u{11F41}\u{11F42}\u{1612F}]/yu;
|