text-sanctifier 1.3.3 → 1.3.5

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.
@@ -1,15 +1,6 @@
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 };
1
+ var d=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+/g,C=/[\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;function I(e){return e<8192?R(e):N(e)}function R(e){return e<174?e<32?e<11?e<1?"nul":e<9?"c0_control":null:e<13?"c0_control":e<14?null:"c0_control":e<160?e<127?null:e<128?"c0_control":"c1_control":e<161?"nonbreaking_space":e<173?null:"invisible_format":e<4449?e<1564?e<847?null:e<848?"invisible_format":null:e<1565?"bidi_control":e<4447?null:"hangul_filler":e<6155?e<6068?null:e<6070?"invisible_format":null:e<6159?e<6158?"variation_selector":"invisible_format":e<6160?"variation_selector":null}function N(e){return e<8304?e<8240?e<8208?e<8203?"nonbreaking_space":e<8206?"zero_width":"bidi_control":e<8234?null:e<8239?"bidi_control":"nonbreaking_space":e<8289?e<8287?null:e<8288?"nonbreaking_space":"zero_width":e<8294?"invisible_format":e<8298?"bidi_control":"invisible_format":e<65040?e<12644?e<12288?null:e<12289?"nonbreaking_space":null:e<12645?"hangul_filler":e<65024?null:"variation_selector":e<65440?e<65279?null:e<65280?"bom":null:e<65520?e<65441?"hangul_filler":null:e<65529?"invisible_format":null}function b(e){return e<917504?e<113828?e<113824?null:"invisible_format":e<119155?null:e<119163?"invisible_format":null:e<917760?e<917632?"tag":"invisible_format":e<918e3?"variation_selector":e<921600?"invisible_format":null}function h(e={}){let r=!!e.normalizeSpaces,n=!!e.purgeZeroWidthSpaces,o=!!e.purgeInvisibleChars,u=!!e.purgeEmojis,i=!!e.nukeControls,l=!!e.keyboardOnlyFilter,c=!!e.normalizeNewlines,f=!!e.trimSpacesAroundNewlines,a=!!e.collapseNewLines,g=!!e.preserveParagraphs,s=!!e.collapseSpaces,E=!!e.finalTrim,F=Number.isFinite(e.maxLength)&&e.maxLength>=0?e.maxLength:1/0,x=e.onMaxLength==="truncate"||e.onMaxLength==="noop"||e.onMaxLength==="throw"?e.onMaxLength:"throw";return t=>_(t,o,u,i,l,c,f,a,g,s,E,F,x,r,n)}h.strict=e=>_(e,!0,!0,!0,!1,!0,!0,!0,!1,!0,!0);h.loose=e=>_(e,!1,!1,!1,!1,!0,!0,!0,!0,!0,!0);h.keyboardOnlyEmoji=e=>_(e,!1,!1,!1,!0,!0,!0,!1,!1,!1,!0);h.keyboardOnly=e=>_(e,!0,!0,!0,!0,!0,!0,!0,!1,!0,!0);h.pasted=e=>_(e,!1,!1,!1,!1,!0,!1,!1,!1,!1,!1,1/0,"throw",!0,!0);function _(e,r=!1,n=!1,o=!1,u=!1,i=!1,l=!1,c=!1,f=!1,a=!1,g=!1,s=1/0,E="throw",F=!1,x=!1){if(typeof e!="string")throw new TypeError("sanctifyText expects a string input.");if(E!=="truncate"&&E!=="noop"&&E!=="throw"&&(E="throw"),e.length>s)switch(E){case"truncate":e=e.slice(0,s);break;case"noop":return e;case"throw":default:throw new RangeError(`sanctifyText input length ${e.length} exceeds maxLength ${s}.`)}let t=e;return F&&(t=t.replace(T," ")),x&&(t=t.replace(L,"")),r&&(t=v(t)),n&&(t=z(t)),o&&(t=q(t)),u&&(t=B(t,n)),i&&(t=H(t)),l&&(t=O(t)),c&&(t=Z(t,f)),a&&(t=V(t)),g?t.trim():t}var T=/[\u00A0\u2000-\u200A\u202F\u205F]/g,L=/[\u200B\u2060\uFEFF]/g;function v(e){return e.replace(C,"")}function D(e){if(p.lastIndex=0,!p.test(e))return"";p.lastIndex=0;let r="";for(let n of e.matchAll(p))r+=n[0];return r}var S=/[^\x20-\x7E\r\n]+/gu;function B(e,r=!1){let n=A(e);return r?n.replace(S,""):n.replace(S,o=>D(o))}var G=/[\u2018\u2019\u201A\u201B\u2032\u2035]/g,X=/[\u201C\u201D\u201E\u201F\u2033\u2036\u00AB\u00BB]/g,k=/[\u2012\u2013\u2014\u2015\u2212]/g,U=/\u2026/g,M=/[\u2022\u00B7]/g,P=/[\uFF01-\uFF5E]/g;function A(e){return e.replace(G,"'").replace(X,'"').replace(k,"-").replace(U,"...").replace(M,"*").replace(P,r=>String.fromCharCode(r.charCodeAt(0)-65248))}var p;try{p=new RegExp("(?:\\p{Extended_Pictographic}(?:\\uFE0F|\\uFE0E)?(?:\\u200D(?:\\p{Extended_Pictographic}|\\w)+)*)","gu")}catch{p=/[\u{2700}-\u{27BF}\u{1F300}-\u{1FAFF}\u{1F3FB}-\u{1F3FF}\u200D\uFE0F\u{1F1E6}-\u{1F1FF}]/gu}function z(e){return e.replace(p,"")}var j=/\r\n|\r|\n/g;function H(e,r=`
2
+ `){return e.replace(j,r)}var K=/[ \t]*(\n+)[ \t]*/g;function O(e){return e.replace(K,"$1")}var W=/\n{2,}/g,J=/\n{3,}/g;function Z(e,r){return r?e.replace(J,`
3
+
4
+ `):e.replace(W,`
5
+ `)}var $=/ {2,}/g;function V(e){return e.replace($," ")}function q(e){return e.replace(d,"")}function m(e,r){return e.lastIndex=0,e.test(r)}function Q(e){let r=A(e);if(!m(S,r))return!1;for(let n of r)if(!(n===`
6
+ `||n==="\r"||n>=" "&&n<="~")&&!m(p,n))return!0;return!1}function Y(e){if(typeof e!="string")throw new TypeError("inspectText expects a string input.");let r=[],n={hasControlChars:!1,hasInvisibleChars:!1,hasMixedNewlines:!1,newlineStyle:null,hasEmojis:!1,hasNonKeyboardChars:!1,summary:r},o=(l,c,f)=>{l&&(n[c]=!0,r.push(f))};o(m(d,e),"hasControlChars","Control characters detected."),o(m(C,e),"hasInvisibleChars","Invisible Unicode characters detected."),o(m(p,e),"hasEmojis","Emojis detected.");let{mixed:u,types:i}=w(e);return n.hasMixedNewlines=u,n.newlineStyle=u?"Mixed":i[0]||null,n.newlineStyle&&r.push(u?"Mixed newline styles detected.":`Consistent newline style: ${n.newlineStyle}`),o(Q(e),"hasNonKeyboardChars","Non-keyboard characters detected."),n}var ee=100;function y(e,r={}){if(typeof e!="string")throw new TypeError("inspectTextDetailed expects a string input.");let n=r&&r.maxFindings,o=n===1/0||Number.isInteger(n)&&n>=0?n:ee,u={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},i=[],l=0,c=1,f=0;for(let a=0,g=e.length;a<g;a++){let s=e.charCodeAt(a);if(s>=32&&s<127)continue;if(s===10){c++,f=a+1;continue}if(s===13){e.charCodeAt(a+1)!==10&&(c++,f=a+1);continue}let E=a,F=s,x;if(s>=55296&&s<=56319){let t=e.charCodeAt(a+1);if(!(t>=56320&&t<=57343))continue;F=65536+(s-55296<<10)+(t-56320),x=b(F),a++}else x=I(s);if(x!==null&&(u[x]++,l++,i.length<o)){let t={offset:E,line:c,column:E-f+1,codePoint:F,category:x};x==="bom"&&(t.atStart=E===0),i.push(t)}}return{findings:i,counts:u,total:l,truncated:l>i.length}}function w(e){if(typeof e!="string")throw new TypeError("getNewlineStats expects a string input.");let r=0,n=0,o=0;for(let l=0,c=e.length;l<c;l++){let f=e.charCodeAt(l);f===13?e.charCodeAt(l+1)===10?(r++,l++):n++:f===10&&o++}let u={crlf:r,cr:n,lf:o},i=[];return u.crlf>0&&i.push("CRLF"),u.cr>0&&i.push("CR"),u.lf>0&&i.push("LF"),{...u,types:i,mixed:i.length>1}}function re(e){return{purgeInvisibleChars:e.hasInvisibleChars,purgeEmojis:e.hasEmojis,nukeControls:e.hasControlChars,keyboardOnlyFilter:e.hasNonKeyboardChars,normalizeNewlines:e.hasMixedNewlines||e.newlineStyle==="CRLF"||e.newlineStyle==="CR"}}function ne(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")+">",o="",u=0;for(let i of y(e,{maxFindings:1/0}).findings)o+=e.slice(u,i.offset)+n(i.codePoint,i.category),u=i.offset+(i.codePoint>65535?2:1);return o+e.slice(u)}export{w as getNewlineStats,re as getRecommendedSanctifierOptions,Y as inspectText,y as inspectTextDetailed,ne as revealInvisible,h as summonSanctifier};
@@ -6,9 +6,11 @@ export type DetailedTrashReport = import("./inspectText.js").DetailedTrashReport
6
6
  export type InspectDetailedOptions = import("./inspectText.js").InspectDetailedOptions;
7
7
  export type NewlineStats = import("./inspectText.js").NewlineStats;
8
8
  export type SanctifyOptions = import("./sanctifyText.js").SanctifyOptions;
9
+ export type RevealOptions = import("./reveal.js").RevealOptions;
9
10
  import { inspectText } from './inspectText.js';
10
11
  import { inspectTextDetailed } from './inspectText.js';
11
12
  import { getNewlineStats } from './inspectText.js';
12
13
  import { getRecommendedSanctifierOptions } from './inspectText.js';
13
14
  import { summonSanctifier } from './sanctifyText.js';
14
- export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier };
15
+ import { revealInvisible } from './reveal.js';
16
+ export { inspectText, inspectTextDetailed, getNewlineStats, getRecommendedSanctifierOptions, summonSanctifier, revealInvisible };
@@ -0,0 +1,26 @@
1
+ /** @typedef {import('./inspectText.js').TrashCategory} TrashCategory */
2
+ /**
3
+ * @typedef {Object} RevealOptions
4
+ * @property {(codePoint: number, category: TrashCategory) => string} [format] - Text shown in place of
5
+ * each hidden character. Default: `<U+XXXX>` (at least four uppercase hex digits).
6
+ */
7
+ /**
8
+ * Makes hidden characters visible: every character `inspectTextDetailed` would report is
9
+ * replaced by a marker such as `<U+200B>`, and everything else is kept as it is,
10
+ * including tabs, newlines and emoji. Use it to show hidden text in logs, diffs, review
11
+ * tools and agent tool output. A literal `<U+200B>` already in the text looks the same as
12
+ * a marker; pass `format` to choose markers that can't occur in your input.
13
+ *
14
+ * @param {string} text
15
+ * @param {RevealOptions} [options]
16
+ * @returns {string}
17
+ */
18
+ export function revealInvisible(text: string, options?: RevealOptions): string;
19
+ export type TrashCategory = import("./inspectText.js").TrashCategory;
20
+ export type RevealOptions = {
21
+ /**
22
+ * - Text shown in place of
23
+ * each hidden character. Default: `<U+XXXX>` (at least four uppercase hex digits).
24
+ */
25
+ format?: (codePoint: number, category: TrashCategory) => string;
26
+ };
@@ -12,6 +12,8 @@
12
12
  * @property {boolean} [finalTrim]
13
13
  * @property {number} [maxLength]
14
14
  * @property {'throw'|'truncate'|'noop'} [onMaxLength]
15
+ * @property {boolean} [normalizeSpaces] - Replace look-alike spaces with an ordinary space (runs first).
16
+ * @property {boolean} [purgeZeroWidthSpaces] - Remove U+200B, U+2060 and U+FEFF only, keeping joiners and selectors.
15
17
  */
16
18
  /**
17
19
  * Summons a customized sanctifier function with pre-bound booleans.
@@ -31,6 +33,8 @@
31
33
  * @param {boolean} [defaultOptions.finalTrim]
32
34
  * @param {number} [defaultOptions.maxLength=Infinity] - Hard input length cap (UTF-16 code units).
33
35
  * @param {'throw' | 'truncate' | 'noop'} [defaultOptions.onMaxLength='throw'] - Behavior when input exceeds maxLength.
36
+ * @param {boolean} [defaultOptions.normalizeSpaces]
37
+ * @param {boolean} [defaultOptions.purgeZeroWidthSpaces]
34
38
  * @returns {(text: string) => string}
35
39
  */
36
40
  export function summonSanctifier(defaultOptions?: {
@@ -46,6 +50,8 @@ export function summonSanctifier(defaultOptions?: {
46
50
  finalTrim?: boolean;
47
51
  maxLength?: number;
48
52
  onMaxLength?: "throw" | "truncate" | "noop";
53
+ normalizeSpaces?: boolean;
54
+ purgeZeroWidthSpaces?: boolean;
49
55
  }): (text: string) => string;
50
56
  export namespace summonSanctifier {
51
57
  /**
@@ -81,6 +87,16 @@ export namespace summonSanctifier {
81
87
  */
82
88
  /** @param {string} text @returns {string} */
83
89
  function keyboardOnly(text: string): string;
90
+ /**
91
+ * Pasted text and model output:
92
+ * - Look-alike spaces (NBSP, narrow NBSP, typographic spaces) become ordinary spaces,
93
+ * so words never run together
94
+ * - Zero-width spaces, word joiners and BOMs are removed
95
+ * - Newlines are normalized to `\n`
96
+ * Keeps emoji joiners, variation selectors, bidi marks, indentation and line breaks.
97
+ */
98
+ /** @param {string} text @returns {string} */
99
+ function pasted(text: string): string;
84
100
  }
85
101
  /**
86
102
  * Text Sanctifier
@@ -106,17 +122,27 @@ export namespace summonSanctifier {
106
122
  * - `'throw'`: throw a `RangeError`
107
123
  * - `'truncate'`: slice to `maxLength` before processing
108
124
  * - `'noop'`: return the original input unchanged (skips processing)
125
+ * @param {boolean} [normalizeSpaces=false] - Replace look-alike spaces with an ordinary space. Runs first,
126
+ * so `purgeInvisibleChars` no longer joins the words around them.
127
+ * @param {boolean} [purgeZeroWidthSpaces=false] - Remove zero-width spaces, word joiners and BOMs only.
109
128
  * @returns {string}
110
129
  * @throws {TypeError} If `text` is not a string.
111
130
  * @throws {RangeError} If input exceeds `maxLength` and `onMaxLength` is `'throw'`.
112
131
  */
113
- export function sanctifyText(text: string, purgeInvisibleChars?: boolean, purgeEmojis?: boolean, nukeControls?: boolean, keyboardOnlyFilter?: boolean, normalizeNewlines?: boolean, trimSpacesAroundNewlines?: boolean, collapseNewLines?: boolean, preserveParagraphs?: boolean, collapseSpaces?: boolean, finalTrim?: boolean, maxLength?: number, onMaxLength?: "throw" | "truncate" | "noop"): string;
132
+ export function sanctifyText(text: string, purgeInvisibleChars?: boolean, purgeEmojis?: boolean, nukeControls?: boolean, keyboardOnlyFilter?: boolean, normalizeNewlines?: boolean, trimSpacesAroundNewlines?: boolean, collapseNewLines?: boolean, preserveParagraphs?: boolean, collapseSpaces?: boolean, finalTrim?: boolean, maxLength?: number, onMaxLength?: "throw" | "truncate" | "noop", normalizeSpaces?: boolean, purgeZeroWidthSpaces?: boolean): string;
114
133
  /**
115
134
  * Normalizes typographic Unicode punctuation into ASCII equivalents.
116
135
  * @param {string} text
117
136
  * @returns {string}
118
137
  */
119
138
  export function normalizeTypographicJank(text: string): string;
139
+ /**
140
+ * Look-alike spaces: the `nonbreaking_space` category except U+3000, the ordinary
141
+ * space of CJK text. A test checks this against the category table.
142
+ */
143
+ export const SPACE_LOOKALIKES_REGEX: RegExp;
144
+ /** Zero-width space, word joiner and BOM: invisible and never needed in plain text. */
145
+ export const ZERO_WIDTH_SPACES_REGEX: RegExp;
120
146
  /**
121
147
  * Matches any character that is NOT:
122
148
  * - Printable ASCII (U+0020–U+007E)
@@ -139,6 +165,14 @@ export type SanctifyOptions = {
139
165
  finalTrim?: boolean;
140
166
  maxLength?: number;
141
167
  onMaxLength?: "throw" | "truncate" | "noop";
168
+ /**
169
+ * - Replace look-alike spaces with an ordinary space (runs first).
170
+ */
171
+ normalizeSpaces?: boolean;
172
+ /**
173
+ * - Remove U+200B, U+2060 and U+FEFF only, keeping joiners and selectors.
174
+ */
175
+ purgeZeroWidthSpaces?: boolean;
142
176
  };
143
177
  import { CONTROL_CHARS_REGEX } from './categories.generated.js';
144
178
  import { INVISIBLE_TRASH_REGEX } from './categories.generated.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-sanctifier",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "description": "A brutal text normalizer and invisible trash scrubber for modern web projects.",
6
6
  "engines": {
@@ -41,12 +41,13 @@
41
41
  "scripts": {
42
42
  "gen": "node scripts/gen-categories.js",
43
43
  "gen:check": "node scripts/gen-categories.js --check",
44
- "build": "node scripts/buildc.js",
44
+ "build": "node scripts/build.js",
45
45
  "build:types": "tsc -p tsconfig.types.json",
46
46
  "test:types": "tsc -p tests/types/tsconfig.json",
47
47
  "test": "node tests/sanctifyText.test.js && node tests/inspectTextDetailed.test.js && node tests/cli.test.js",
48
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"
49
+ "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",
50
+ "size": "node scripts/check-size.js"
50
51
  },
51
52
  "keywords": [
52
53
  "text",
@@ -73,8 +74,6 @@
73
74
  },
74
75
  "devDependencies": {
75
76
  "esbuild": "^0.25.3",
76
- "google-closure-compiler": "^20240317.0.0",
77
- "terser": "^5.39.0",
78
77
  "typescript": "^5.9.3"
79
78
  }
80
79
  }