text-sanctifier 1.3.12 → 1.3.13

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 CHANGED
@@ -261,6 +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
+ - `sequence` marks a lone selector in a form `text-sanctifier/context` calls benign: `'ideographic'` for U+E0100–U+E01EF after an ideograph, `'mongolian'` for U+180B–U+180D or U+180F after a Mongolian letter. These stay in `runs` because they can carry data, up to a byte per ideograph, so a run with `sequence` is usually ordinary text, but many of them can be a message. U+FE0E and U+FE0F have no such room and go under `benign`.
264
265
  - `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
266
  - `decodeTagText` turns U+E0020–U+E007E into the ASCII they shadow and writes any other character as `<U+XXXX>`.
266
267
  - Zero-width, bidi and other invisible characters are not runs; use `inspectTextDetailed` for those.
@@ -365,7 +366,7 @@ The repository publishes a [pre-commit](https://pre-commit.com) hook manifest, s
365
366
  # .pre-commit-config.yaml
366
367
  repos:
367
368
  - repo: https://github.com/iWhatty/text-sanctifier
368
- rev: v1.3.12 # a release tag
369
+ rev: v1.3.13 # a release tag
369
370
  hooks:
370
371
  - id: text-sanctifier # every staged text file
371
372
  - id: text-sanctifier-ai-config # agent instruction files, with --ai-config
@@ -3,4 +3,4 @@ var y=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u2
3
3
  `){return e.replace(re,r)}var te=/[ \t]*(\n+)[ \t]*|([ \t]+)/g;function ie(e){return e.replace(te,"$1$2")}var oe=/\n{2,}/g,ue=/\n{3,}/g;function se(e,r){return r?e.replace(ue,`
4
4
 
5
5
  `):e.replace(oe,`
6
- `)}var le=/ {2,}/g;function ae(e){return e.replace(le," ")}function fe(e){return e.replace(y,"")}function ce(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 A(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 Ee="1.3.12";export{Ee as VERSION,R as getNewlineStats,H as getRecommendedSanctifierOptions,j as inspectText,A as inspectTextDetailed,ce as revealInvisible,L as sanctifyKeyboardOnly,T as sanctifyKeyboardOnlyEmoji,N as sanctifyLoose,D as sanctifyPasted,w as sanctifyStrict,J as summonSanctifier};
6
+ `)}var le=/ {2,}/g;function ae(e){return e.replace(le," ")}function fe(e){return e.replace(y,"")}function ce(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 A(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 Ee="1.3.13";export{Ee as VERSION,R as getNewlineStats,H as getRecommendedSanctifierOptions,j as inspectText,A as inspectTextDetailed,ce as revealInvisible,L as sanctifyKeyboardOnly,T as sanctifyKeyboardOnlyEmoji,N as sanctifyLoose,D as sanctifyPasted,w as sanctifyStrict,J as summonSanctifier};
@@ -25,6 +25,7 @@ export function scanHiddenSpans(text: string, options?: HiddenScanOptions, onSpa
25
25
  }) => void): HiddenTextScan;
26
26
  export type HiddenRunKind = import("./hiddenText.js").HiddenRunKind;
27
27
  export type HiddenRun = import("./hiddenText.js").HiddenRun;
28
+ export type HiddenRunSequence = import("./hiddenText.js").HiddenRunSequence;
28
29
  export type HiddenRunCounts = import("./hiddenText.js").HiddenRunCounts;
29
30
  export type HiddenTextScan = import("./hiddenText.js").HiddenTextScan;
30
31
  export type HiddenScanOptions = import("./hiddenText.js").HiddenScanOptions;
@@ -1,4 +1,5 @@
1
1
  /** @typedef {'tag' | 'variation_selector' | 'invisible_operator' | 'reserved_tag_plane'} HiddenRunKind */
2
+ /** @typedef {'ideographic' | 'mongolian'} HiddenRunSequence */
2
3
  /**
3
4
  * @typedef {Object} HiddenRun
4
5
  * @property {HiddenRunKind} kind
@@ -10,6 +11,9 @@
10
11
  * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText),
11
12
  * at most `maxDecodedLength` UTF-16 units
12
13
  * @property {boolean} [decodedTruncated] - tag runs only: true when `decoded` stops short of the run
14
+ * @property {HiddenRunSequence} [sequence] - a lone selector in a form text-sanctifier/context calls
15
+ * benign: U+E0100–U+E01EF after an ideograph, or U+180B–U+180D or U+180F after a Mongolian letter.
16
+ * Still a run, because these can carry data: up to a byte per ideograph.
13
17
  */
14
18
  /**
15
19
  * @typedef {Object} HiddenRunCounts
@@ -57,6 +61,7 @@ export function decodeTagText(text: string): string;
57
61
  * Extended_Pictographic character, or between #, * or a digit and U+20E3 (a keycap),
58
62
  * the same rule as text-sanctifier/context. After a letter it is a run: one selector
59
63
  * per letter can carry a bit each.
64
+ * Lone ideographic and Mongolian selectors stay runs, marked with `sequence`.
60
65
  *
61
66
  * `line` and `column` are counted as in inspectTextDetailed. Every run and benign
62
67
  * span is counted; only the first `maxRuns` of each are listed. With the caps set,
@@ -70,6 +75,7 @@ export function decodeTagText(text: string): string;
70
75
  export function findHiddenRuns(text: string, options?: HiddenScanOptions): HiddenTextScan;
71
76
  export { VERSION } from "./version.generated.js";
72
77
  export type HiddenRunKind = "tag" | "variation_selector" | "invisible_operator" | "reserved_tag_plane";
78
+ export type HiddenRunSequence = "ideographic" | "mongolian";
73
79
  export type HiddenRun = {
74
80
  kind: HiddenRunKind;
75
81
  /**
@@ -101,6 +107,12 @@ export type HiddenRun = {
101
107
  * - tag runs only: true when `decoded` stops short of the run
102
108
  */
103
109
  decodedTruncated?: boolean;
110
+ /**
111
+ * - a lone selector in a form text-sanctifier/context calls
112
+ * benign: U+E0100–U+E01EF after an ideograph, or U+180B–U+180D or U+180F after a Mongolian letter.
113
+ * Still a run, because these can carry data: up to a byte per ideograph.
114
+ */
115
+ sequence?: HiddenRunSequence;
104
116
  };
105
117
  export type HiddenRunCounts = {
106
118
  tag: number;
@@ -1,2 +1,2 @@
1
1
  /** The installed version of text-sanctifier, as in its package.json. */
2
- export const VERSION: "1.3.12";
2
+ export const VERSION: "1.3.13";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-sanctifier",
3
- "version": "1.3.12",
3
+ "version": "1.3.13",
4
4
  "type": "module",
5
5
  "description": "Find and remove invisible Unicode in web input, model-bound text and repositories.",
6
6
  "engines": {
package/src/hiddenScan.js CHANGED
@@ -3,6 +3,7 @@
3
3
  /**
4
4
  * @typedef {import('./hiddenText.js').HiddenRunKind} HiddenRunKind
5
5
  * @typedef {import('./hiddenText.js').HiddenRun} HiddenRun
6
+ * @typedef {import('./hiddenText.js').HiddenRunSequence} HiddenRunSequence
6
7
  * @typedef {import('./hiddenText.js').HiddenRunCounts} HiddenRunCounts
7
8
  * @typedef {import('./hiddenText.js').HiddenTextScan} HiddenTextScan
8
9
  * @typedef {import('./hiddenText.js').HiddenScanOptions} HiddenScanOptions
@@ -76,6 +77,32 @@ function isPresentationBase(text, index) {
76
77
  return PICTOGRAPHIC_AT.test(text);
77
78
  }
78
79
 
80
+ const IDEOGRAPHIC_AT = /\p{Ideographic}/yu;
81
+
82
+ /**
83
+ * The legitimate sequence a lone selector at `index` could belong to, by the same rules
84
+ * as text-sanctifier/context. It stays a run: unlike U+FE0E and U+FE0F, these selectors
85
+ * have enough values to spell data, up to a byte per ideograph.
86
+ * @param {string} text
87
+ * @param {number} index
88
+ * @param {number} cp - the selector
89
+ * @returns {HiddenRunSequence | null}
90
+ */
91
+ function sequenceOf(text, index, cp) {
92
+ if (cp >= 0xE0100 && cp <= 0xE01EF) {
93
+ if (index === 0) return null;
94
+ const low = text.charCodeAt(index - 1);
95
+ IDEOGRAPHIC_AT.lastIndex = low >= 0xDC00 && low <= 0xDFFF && index >= 2 &&
96
+ (text.charCodeAt(index - 2) & 0xFC00) === 0xD800 ? index - 2 : index - 1;
97
+ return IDEOGRAPHIC_AT.test(text) ? 'ideographic' : null;
98
+ }
99
+ if (cp > 0x180F) return null;
100
+ const c = text.charCodeAt(index - 1);
101
+ return (c >= 0x1820 && c <= 0x1878) || (c >= 0x1880 && c <= 0x1884) || (c >= 0x1887 && c <= 0x18A8) || c === 0x18AA
102
+ ? 'mongolian'
103
+ : null;
104
+ }
105
+
79
106
  /**
80
107
  * A cap option: a non-negative integer or Infinity; anything else means no cap.
81
108
  * @param {unknown} value
@@ -163,6 +190,9 @@ export function scanHiddenSpans(text, options = {}, onSpan) {
163
190
  const { decoded, truncated } = decodeRange(text, offset, end, maxDecodedLength);
164
191
  run.decoded = decoded;
165
192
  run.decodedTruncated = truncated;
193
+ } else if (kind === 'variation_selector' && count === 1) {
194
+ const sequence = sequenceOf(text, offset, cp);
195
+ if (sequence) run.sequence = sequence;
166
196
  }
167
197
  runs.push(run);
168
198
  }
package/src/hiddenText.js CHANGED
@@ -15,6 +15,8 @@ export { VERSION } from './version.generated.js';
15
15
 
16
16
  /** @typedef {'tag' | 'variation_selector' | 'invisible_operator' | 'reserved_tag_plane'} HiddenRunKind */
17
17
 
18
+ /** @typedef {'ideographic' | 'mongolian'} HiddenRunSequence */
19
+
18
20
  /**
19
21
  * @typedef {Object} HiddenRun
20
22
  * @property {HiddenRunKind} kind
@@ -26,6 +28,9 @@ export { VERSION } from './version.generated.js';
26
28
  * @property {string} [decoded] - tag runs only: the ASCII the tags spell (see decodeTagText),
27
29
  * at most `maxDecodedLength` UTF-16 units
28
30
  * @property {boolean} [decodedTruncated] - tag runs only: true when `decoded` stops short of the run
31
+ * @property {HiddenRunSequence} [sequence] - a lone selector in a form text-sanctifier/context calls
32
+ * benign: U+E0100–U+E01EF after an ideograph, or U+180B–U+180D or U+180F after a Mongolian letter.
33
+ * Still a run, because these can carry data: up to a byte per ideograph.
29
34
  */
30
35
 
31
36
  /**
@@ -81,6 +86,7 @@ export function decodeTagText(text) {
81
86
  * Extended_Pictographic character, or between #, * or a digit and U+20E3 (a keycap),
82
87
  * the same rule as text-sanctifier/context. After a letter it is a run: one selector
83
88
  * per letter can carry a bit each.
89
+ * Lone ideographic and Mongolian selectors stay runs, marked with `sequence`.
84
90
  *
85
91
  * `line` and `column` are counted as in inspectTextDetailed. Every run and benign
86
92
  * span is counted; only the first `maxRuns` of each are listed. With the caps set,
@@ -2,4 +2,4 @@
2
2
  // Generated from package.json by scripts/gen-version.js (`npm run gen`); do not edit.
3
3
 
4
4
  /** The installed version of text-sanctifier, as in its package.json. */
5
- export const VERSION = '1.3.12';
5
+ export const VERSION = '1.3.13';