deghost 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,413 @@
1
+ // src/categories.ts
2
+ var patterns = {
3
+ /** Format characters: zero-width joiners, directional marks, soft hyphens, etc. */
4
+ format: /\p{Cf}/gu,
5
+ /** Control characters: C0 (0x00–0x1F) and C1 (0x7F–0x9F) controls. */
6
+ control: /\p{Cc}/gu,
7
+ /**
8
+ * Space separators: NBSP, en/em/thin/hair/ideographic space, etc.
9
+ * Excludes U+0020 (regular ASCII space) — that's not a ghost.
10
+ */
11
+ spaces: /(?!\u0020)\p{Zs}/gu,
12
+ /** Tag characters: deprecated Unicode tag block (U+E0001–U+E007F). */
13
+ tag: /[\u{E0001}-\u{E007F}]/gu,
14
+ /** Byte order mark / zero-width no-break space. */
15
+ bom: /\uFEFF/gu,
16
+ /**
17
+ * Script-specific filler characters:
18
+ * - U+115F, U+1160: Hangul Choseong/Jungseong fillers
19
+ * - U+3164: Hangul filler
20
+ * - U+FFA0: Halfwidth Hangul filler
21
+ * - U+17B4, U+17B5: Khmer vowel inherent
22
+ * - U+180E: Mongolian vowel separator
23
+ * - U+1680: Ogham space mark
24
+ */
25
+ fillers: /[\u115F\u1160\u3164\uFFA0\u17B4\u17B5\u180E\u1680]/gu,
26
+ /**
27
+ * Invisible math operators:
28
+ * - U+2061: Function application
29
+ * - U+2062: Invisible times
30
+ * - U+2063: Invisible separator
31
+ * - U+2064: Invisible plus
32
+ */
33
+ math: /[\u2061-\u2064]/gu
34
+ };
35
+ var categories = Object.freeze(Object.keys(patterns));
36
+ var descriptions = {
37
+ format: "Format characters (zero-width joiners, directional marks, soft hyphens)",
38
+ control: "Control characters (C0/C1 controls)",
39
+ spaces: "Space separators (NBSP, en/em space, thin space, ideographic space)",
40
+ tag: "Unicode tag characters",
41
+ bom: "Byte order mark",
42
+ fillers: "Script-specific filler characters (Hangul, Khmer, Mongolian, Ogham)",
43
+ math: "Invisible math operators"
44
+ };
45
+ var charNames = {
46
+ 173: "SOFT HYPHEN",
47
+ 847: "COMBINING GRAPHEME JOINER",
48
+ 1564: "ARABIC LETTER MARK",
49
+ 160: "NO-BREAK SPACE",
50
+ 5760: "OGHAM SPACE MARK",
51
+ 8192: "EN QUAD",
52
+ 8193: "EM QUAD",
53
+ 8194: "EN SPACE",
54
+ 8195: "EM SPACE",
55
+ 8196: "THREE-PER-EM SPACE",
56
+ 8197: "FOUR-PER-EM SPACE",
57
+ 8198: "SIX-PER-EM SPACE",
58
+ 8199: "FIGURE SPACE",
59
+ 8200: "PUNCTUATION SPACE",
60
+ 8201: "THIN SPACE",
61
+ 8202: "HAIR SPACE",
62
+ 8203: "ZERO WIDTH SPACE",
63
+ 8204: "ZERO WIDTH NON-JOINER",
64
+ 8205: "ZERO WIDTH JOINER",
65
+ 8206: "LEFT-TO-RIGHT MARK",
66
+ 8207: "RIGHT-TO-LEFT MARK",
67
+ 8232: "LINE SEPARATOR",
68
+ 8233: "PARAGRAPH SEPARATOR",
69
+ 8234: "LEFT-TO-RIGHT EMBEDDING",
70
+ 8235: "RIGHT-TO-LEFT EMBEDDING",
71
+ 8236: "POP DIRECTIONAL FORMATTING",
72
+ 8237: "LEFT-TO-RIGHT OVERRIDE",
73
+ 8238: "RIGHT-TO-LEFT OVERRIDE",
74
+ 8239: "NARROW NO-BREAK SPACE",
75
+ 8287: "MEDIUM MATHEMATICAL SPACE",
76
+ 8288: "WORD JOINER",
77
+ 8289: "FUNCTION APPLICATION",
78
+ 8290: "INVISIBLE TIMES",
79
+ 8291: "INVISIBLE SEPARATOR",
80
+ 8292: "INVISIBLE PLUS",
81
+ 8294: "LEFT-TO-RIGHT ISOLATE",
82
+ 8295: "RIGHT-TO-LEFT ISOLATE",
83
+ 8296: "FIRST STRONG ISOLATE",
84
+ 8297: "POP DIRECTIONAL ISOLATE",
85
+ 12288: "IDEOGRAPHIC SPACE",
86
+ 12644: "HANGUL FILLER",
87
+ 65279: "ZERO WIDTH NO-BREAK SPACE",
88
+ 65440: "HALFWIDTH HANGUL FILLER",
89
+ 4447: "HANGUL CHOSEONG FILLER",
90
+ 4448: "HANGUL JUNGSEONG FILLER",
91
+ 6068: "KHMER VOWEL INHERENT AQ",
92
+ 6069: "KHMER VOWEL INHERENT AA",
93
+ 6158: "MONGOLIAN VOWEL SEPARATOR"
94
+ };
95
+ var fillerSet = /* @__PURE__ */ new Set([4447, 4448, 12644, 65440, 6068, 6069, 6158, 5760]);
96
+ var isZs = /^\p{Zs}$/u;
97
+ var isCf = /^\p{Cf}$/u;
98
+ var isCc = /^\p{Cc}$/u;
99
+ function categorize(codepoint) {
100
+ if (codepoint === 65279) return "bom";
101
+ if (codepoint >= 917505 && codepoint <= 917631) return "tag";
102
+ if (codepoint >= 8289 && codepoint <= 8292) return "math";
103
+ if (fillerSet.has(codepoint)) return "fillers";
104
+ if (codepoint === 32) return void 0;
105
+ const char = String.fromCodePoint(codepoint);
106
+ if (isZs.test(char)) return "spaces";
107
+ if (isCf.test(char)) return "format";
108
+ if (isCc.test(char)) return "control";
109
+ return void 0;
110
+ }
111
+
112
+ // src/detect.ts
113
+ var formatHex = (cp) => `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
114
+ var regexCache = /* @__PURE__ */ new Map();
115
+ function getRegex(categories2) {
116
+ const cats = categories2 ?? Object.keys(patterns);
117
+ const key = [...cats].sort().join(",");
118
+ let cached = regexCache.get(key);
119
+ if (!cached) {
120
+ cached = new RegExp(cats.map((c) => patterns[c].source).join("|"), "gu");
121
+ regexCache.set(key, cached);
122
+ }
123
+ return cached;
124
+ }
125
+ function* scan(input, categories2) {
126
+ const regex = getRegex(categories2);
127
+ regex.lastIndex = 0;
128
+ for (const match of input.matchAll(regex)) {
129
+ const char = match[0];
130
+ const cp = char.codePointAt(0);
131
+ const category = categorize(cp);
132
+ if (category === void 0) continue;
133
+ const hex = formatHex(cp);
134
+ yield {
135
+ char,
136
+ codepoint: hex,
137
+ name: charNames[cp] ?? hex,
138
+ category,
139
+ offset: match.index
140
+ };
141
+ }
142
+ }
143
+ function detect(input, categories2) {
144
+ return [...scan(input, categories2)];
145
+ }
146
+ function first(input, categories2) {
147
+ for (const d of scan(input, categories2)) return d;
148
+ return void 0;
149
+ }
150
+ function hasGhosts(input, categories2) {
151
+ const regex = getRegex(categories2);
152
+ regex.lastIndex = 0;
153
+ return regex.test(input);
154
+ }
155
+ function isClean(input, categories2) {
156
+ return !hasGhosts(input, categories2);
157
+ }
158
+ function count(input, categories2) {
159
+ const result = {};
160
+ for (const d of detect(input, categories2)) {
161
+ result[d.category] = (result[d.category] ?? 0) + 1;
162
+ }
163
+ return result;
164
+ }
165
+ function identify(input) {
166
+ const cp = typeof input === "number" ? input : input.codePointAt(0);
167
+ const category = categorize(cp);
168
+ if (category === void 0) return void 0;
169
+ const hex = formatHex(cp);
170
+ return {
171
+ codepoint: hex,
172
+ name: charNames[cp] ?? hex,
173
+ category
174
+ };
175
+ }
176
+
177
+ // src/replace.ts
178
+ var defaultFormatter = (d) => `[${d.codepoint}]`;
179
+ function replaceDetections(input, detections, mapper) {
180
+ if (detections.length === 0) return input;
181
+ const parts = [];
182
+ let cursor = 0;
183
+ for (const d of detections) {
184
+ parts.push(input.slice(cursor, d.offset));
185
+ parts.push(mapper(d));
186
+ cursor = d.offset + d.char.length;
187
+ }
188
+ parts.push(input.slice(cursor));
189
+ return parts.join("");
190
+ }
191
+
192
+ // src/summary.ts
193
+ function summary(input, categories2) {
194
+ const detections = detect(input, categories2);
195
+ if (detections.length === 0) return "No invisible characters found.";
196
+ const counts = {};
197
+ for (const d of detections) {
198
+ counts[d.category] = (counts[d.category] ?? 0) + 1;
199
+ }
200
+ const total = detections.length;
201
+ const header = `${total} invisible character${total === 1 ? "" : "s"} found.`;
202
+ const byCategory = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)).map(([cat, n]) => ` ${cat}: ${n}`).join("\n");
203
+ const details = detections.map((d) => ` ${d.codepoint} ${d.name} (${d.category}, offset ${d.offset})`).join("\n");
204
+ return `${header}
205
+
206
+ By category:
207
+ ${byCategory}
208
+
209
+ Details:
210
+ ${details}`;
211
+ }
212
+
213
+ // src/chain.ts
214
+ var DeghostChain = class _DeghostChain {
215
+ #value;
216
+ constructor(value) {
217
+ this.#value = value;
218
+ }
219
+ /** Remove all characters in the given category. */
220
+ strip(category) {
221
+ return new _DeghostChain(this.#value.replace(patterns[category], ""));
222
+ }
223
+ /** Replace all characters in the given category with a substitute. */
224
+ normalize(category, replacement = " ") {
225
+ return new _DeghostChain(this.#value.replace(patterns[category], replacement));
226
+ }
227
+ /** Replace matched ghosts in a category using a mapper function. */
228
+ replace(category, mapper) {
229
+ const result = replaceDetections(this.#value, detect(this.#value, [category]), mapper);
230
+ return result === this.#value ? this : new _DeghostChain(result);
231
+ }
232
+ /** Replace ghosts with visible markers like `[U+200B]`. */
233
+ highlight(category, formatter = defaultFormatter) {
234
+ const categories2 = category ? [category] : void 0;
235
+ const result = replaceDetections(
236
+ this.#value,
237
+ detect(this.#value, categories2),
238
+ formatter
239
+ );
240
+ return result === this.#value ? this : new _DeghostChain(result);
241
+ }
242
+ /** Return detections for the current chain value. */
243
+ detect(categories2) {
244
+ return detect(this.#value, categories2);
245
+ }
246
+ /** Check if the current chain value contains invisible characters. */
247
+ hasGhosts(categories2) {
248
+ return hasGhosts(this.#value, categories2);
249
+ }
250
+ /** Count invisible characters by category in the current chain value. */
251
+ count(categories2) {
252
+ return count(this.#value, categories2);
253
+ }
254
+ /** Returns true if the current chain value has no invisible characters. */
255
+ isClean(categories2) {
256
+ return isClean(this.#value, categories2);
257
+ }
258
+ /** Return a human-readable report of ghosts in the current chain value. */
259
+ summary(categories2) {
260
+ return summary(this.#value, categories2);
261
+ }
262
+ /** Collapse runs of whitespace into a single space. */
263
+ collapse() {
264
+ return new _DeghostChain(this.#value.replace(/ {2,}/g, " "));
265
+ }
266
+ /** Trim leading and trailing whitespace. */
267
+ trim() {
268
+ return new _DeghostChain(this.#value.trim());
269
+ }
270
+ /** Apply the default cleaning preset: strip format + control, normalize spaces, trim. */
271
+ clean() {
272
+ return this.strip("format").strip("control").strip("bom").normalize("spaces").collapse().trim();
273
+ }
274
+ /** Extract the cleaned string. */
275
+ toString() {
276
+ return this.#value;
277
+ }
278
+ /** Extract the cleaned string (alias for toString). */
279
+ valueOf() {
280
+ return this.#value;
281
+ }
282
+ /** Support JSON.stringify. */
283
+ toJSON() {
284
+ return this.#value;
285
+ }
286
+ };
287
+
288
+ // src/cleaner.ts
289
+ var CleanerBuilder = class {
290
+ #rules = [];
291
+ #trim = false;
292
+ #collapse = false;
293
+ /** Add a strip rule — remove all characters in this category. */
294
+ strip(category) {
295
+ this.#rules.push({ category, action: "strip" });
296
+ return this;
297
+ }
298
+ /** Add a normalize rule — replace characters in this category. */
299
+ normalize(category, replacement = " ") {
300
+ this.#rules.push({ category, action: "normalize", replacement });
301
+ return this;
302
+ }
303
+ /** Add a replace rule — transform characters using detection metadata. */
304
+ replace(category, mapper) {
305
+ this.#rules.push({ category, action: "replace", mapper });
306
+ return this;
307
+ }
308
+ /** Add a highlight step — annotate characters with visible markers. */
309
+ highlight(category, formatter = defaultFormatter) {
310
+ this.#rules.push({ category, action: "replace", mapper: formatter });
311
+ return this;
312
+ }
313
+ /** Enable whitespace trimming as a final step. */
314
+ trim() {
315
+ this.#trim = true;
316
+ return this;
317
+ }
318
+ /** Enable collapsing runs of whitespace as a final step. */
319
+ collapse() {
320
+ this.#collapse = true;
321
+ return this;
322
+ }
323
+ /** Compile the pipeline into a reusable function. */
324
+ build() {
325
+ const steps = this.#rules.map((rule) => {
326
+ if (rule.action === "replace") {
327
+ const { mapper, category } = rule;
328
+ return (s) => replaceDetections(s, detect(s, [category]), mapper);
329
+ }
330
+ const pattern = patterns[rule.category];
331
+ if (rule.action === "strip") {
332
+ return (s) => s.replace(pattern, "");
333
+ }
334
+ const replacement = rule.replacement ?? " ";
335
+ return (s) => s.replace(pattern, replacement);
336
+ });
337
+ const doCollapse = this.#collapse;
338
+ const doTrim = this.#trim;
339
+ return (input) => {
340
+ let result = input;
341
+ for (const step of steps) {
342
+ result = step(result);
343
+ }
344
+ if (doCollapse) result = result.replace(/ {2,}/g, " ");
345
+ if (doTrim) result = result.trim();
346
+ return result;
347
+ };
348
+ }
349
+ };
350
+ function cleaner() {
351
+ return new CleanerBuilder();
352
+ }
353
+
354
+ // src/presets.ts
355
+ var presets = {
356
+ /**
357
+ * Default clean: strip format + control + BOM, normalize spaces, collapse, trim.
358
+ *
359
+ * The right choice for most text processing — catches invisible chars from
360
+ * binary formats (Garmin FIT, PDFs), APIs, and copy-paste while preserving
361
+ * word boundaries.
362
+ */
363
+ clean: cleaner().strip("format").strip("control").strip("bom").normalize("spaces").collapse().trim().build(),
364
+ /**
365
+ * Aggressive: strip everything invisible, including fillers, math operators, and tags.
366
+ *
367
+ * Use when you want maximally clean output and don't need to preserve any
368
+ * invisible Unicode semantics (ligature joiners, bidi marks, etc.).
369
+ */
370
+ aggressive: cleaner().strip("format").strip("control").strip("bom").strip("tag").strip("fillers").strip("math").normalize("spaces").collapse().trim().build(),
371
+ /**
372
+ * Spaces only: normalize Unicode whitespace to ASCII space.
373
+ *
374
+ * Leaves format/control characters alone. Useful when you only care about
375
+ * NBSP and exotic spaces (common in data from Garmin, Strava, etc.).
376
+ */
377
+ spaces: cleaner().normalize("spaces").collapse().trim().build()
378
+ };
379
+
380
+ // src/highlight.ts
381
+ function highlight(input, options) {
382
+ const formatter = typeof options === "function" ? options : options?.formatter ?? defaultFormatter;
383
+ const categories2 = typeof options === "object" ? options.categories : void 0;
384
+ return replaceDetections(input, detect(input, categories2), formatter);
385
+ }
386
+
387
+ // src/index.ts
388
+ function deghost(input, ...rest) {
389
+ let raw;
390
+ if (typeof input === "string") {
391
+ raw = input;
392
+ } else {
393
+ raw = input[0] ?? "";
394
+ for (let i = 0; i < rest.length; i++) {
395
+ raw += String(rest[i]) + (input[i + 1] ?? "");
396
+ }
397
+ }
398
+ const options = typeof input === "string" && rest.length <= 1 ? rest[0] : void 0;
399
+ const chain = new DeghostChain(raw);
400
+ const cleaned = presets.clean(raw);
401
+ const trimmed = options?.trim ?? true ? cleaned.trim() : cleaned;
402
+ return new Proxy(chain, {
403
+ get(target, prop, receiver) {
404
+ if (prop === Symbol.toPrimitive) return () => trimmed;
405
+ if (prop === "length") return trimmed.length;
406
+ return Reflect.get(target, prop, receiver);
407
+ }
408
+ });
409
+ }
410
+
411
+ export { CleanerBuilder, DeghostChain, categories, categorize, charNames, cleaner, count, deghost, descriptions, detect, first, hasGhosts, highlight, identify, isClean, patterns, presets, scan, summary };
412
+ //# sourceMappingURL=index.js.map
413
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/categories.ts","../src/detect.ts","../src/replace.ts","../src/summary.ts","../src/chain.ts","../src/cleaner.ts","../src/presets.ts","../src/highlight.ts","../src/index.ts"],"names":["categories"],"mappings":"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","file":"index.js","sourcesContent":["import type { Category } from './types'\n\n/**\n * Regex patterns for each invisible character category.\n *\n * Uses ES2018 Unicode property escapes where possible (\\p{Cf}, \\p{Cc}, \\p{Zs}).\n * Falls back to explicit codepoint ranges for categories not covered by a\n * single Unicode general category.\n */\nexport const patterns: Record<Category, RegExp> = {\n /** Format characters: zero-width joiners, directional marks, soft hyphens, etc. */\n format: /\\p{Cf}/gu,\n\n /** Control characters: C0 (0x00–0x1F) and C1 (0x7F–0x9F) controls. */\n control: /\\p{Cc}/gu,\n\n /**\n * Space separators: NBSP, en/em/thin/hair/ideographic space, etc.\n * Excludes U+0020 (regular ASCII space) — that's not a ghost.\n */\n spaces: /(?!\\u0020)\\p{Zs}/gu,\n\n /** Tag characters: deprecated Unicode tag block (U+E0001–U+E007F). */\n tag: /[\\u{E0001}-\\u{E007F}]/gu,\n\n /** Byte order mark / zero-width no-break space. */\n bom: /\\uFEFF/gu,\n\n /**\n * Script-specific filler characters:\n * - U+115F, U+1160: Hangul Choseong/Jungseong fillers\n * - U+3164: Hangul filler\n * - U+FFA0: Halfwidth Hangul filler\n * - U+17B4, U+17B5: Khmer vowel inherent\n * - U+180E: Mongolian vowel separator\n * - U+1680: Ogham space mark\n */\n fillers: /[\\u115F\\u1160\\u3164\\uFFA0\\u17B4\\u17B5\\u180E\\u1680]/gu,\n\n /**\n * Invisible math operators:\n * - U+2061: Function application\n * - U+2062: Invisible times\n * - U+2063: Invisible separator\n * - U+2064: Invisible plus\n */\n math: /[\\u2061-\\u2064]/gu,\n}\n\n/** All category names as a readonly array. */\nexport const categories: readonly Category[] = Object.freeze(Object.keys(patterns) as Category[])\n\n/** Human-readable descriptions for each category. */\nexport const descriptions: Record<Category, string> = {\n format: 'Format characters (zero-width joiners, directional marks, soft hyphens)',\n control: 'Control characters (C0/C1 controls)',\n spaces: 'Space separators (NBSP, en/em space, thin space, ideographic space)',\n tag: 'Unicode tag characters',\n bom: 'Byte order mark',\n fillers: 'Script-specific filler characters (Hangul, Khmer, Mongolian, Ogham)',\n math: 'Invisible math operators',\n}\n\n/**\n * Unicode character names for well-known invisible codepoints.\n *\n * This is not exhaustive — \\p{Cf} alone covers 170+ characters.\n * We map the ~40 most commonly encountered ones for the detect() API.\n */\nexport const charNames: Record<number, string> = {\n 0x00ad: 'SOFT HYPHEN',\n 0x034f: 'COMBINING GRAPHEME JOINER',\n 0x061c: 'ARABIC LETTER MARK',\n 0x00a0: 'NO-BREAK SPACE',\n 0x1680: 'OGHAM SPACE MARK',\n 0x2000: 'EN QUAD',\n 0x2001: 'EM QUAD',\n 0x2002: 'EN SPACE',\n 0x2003: 'EM SPACE',\n 0x2004: 'THREE-PER-EM SPACE',\n 0x2005: 'FOUR-PER-EM SPACE',\n 0x2006: 'SIX-PER-EM SPACE',\n 0x2007: 'FIGURE SPACE',\n 0x2008: 'PUNCTUATION SPACE',\n 0x2009: 'THIN SPACE',\n 0x200a: 'HAIR SPACE',\n 0x200b: 'ZERO WIDTH SPACE',\n 0x200c: 'ZERO WIDTH NON-JOINER',\n 0x200d: 'ZERO WIDTH JOINER',\n 0x200e: 'LEFT-TO-RIGHT MARK',\n 0x200f: 'RIGHT-TO-LEFT MARK',\n 0x2028: 'LINE SEPARATOR',\n 0x2029: 'PARAGRAPH SEPARATOR',\n 0x202a: 'LEFT-TO-RIGHT EMBEDDING',\n 0x202b: 'RIGHT-TO-LEFT EMBEDDING',\n 0x202c: 'POP DIRECTIONAL FORMATTING',\n 0x202d: 'LEFT-TO-RIGHT OVERRIDE',\n 0x202e: 'RIGHT-TO-LEFT OVERRIDE',\n 0x202f: 'NARROW NO-BREAK SPACE',\n 0x205f: 'MEDIUM MATHEMATICAL SPACE',\n 0x2060: 'WORD JOINER',\n 0x2061: 'FUNCTION APPLICATION',\n 0x2062: 'INVISIBLE TIMES',\n 0x2063: 'INVISIBLE SEPARATOR',\n 0x2064: 'INVISIBLE PLUS',\n 0x2066: 'LEFT-TO-RIGHT ISOLATE',\n 0x2067: 'RIGHT-TO-LEFT ISOLATE',\n 0x2068: 'FIRST STRONG ISOLATE',\n 0x2069: 'POP DIRECTIONAL ISOLATE',\n 0x3000: 'IDEOGRAPHIC SPACE',\n 0x3164: 'HANGUL FILLER',\n 0xfeff: 'ZERO WIDTH NO-BREAK SPACE',\n 0xffa0: 'HALFWIDTH HANGUL FILLER',\n 0x115f: 'HANGUL CHOSEONG FILLER',\n 0x1160: 'HANGUL JUNGSEONG FILLER',\n 0x17b4: 'KHMER VOWEL INHERENT AQ',\n 0x17b5: 'KHMER VOWEL INHERENT AA',\n 0x180e: 'MONGOLIAN VOWEL SEPARATOR',\n}\n\nconst fillerSet = new Set([0x115f, 0x1160, 0x3164, 0xffa0, 0x17b4, 0x17b5, 0x180e, 0x1680])\nconst isZs = /^\\p{Zs}$/u\nconst isCf = /^\\p{Cf}$/u\nconst isCc = /^\\p{Cc}$/u\n\n/** Resolve the deghost category for a given codepoint. */\nexport function categorize(codepoint: number): Category | undefined {\n if (codepoint === 0xfeff) return 'bom'\n if (codepoint >= 0xe0001 && codepoint <= 0xe007f) return 'tag'\n if (codepoint >= 0x2061 && codepoint <= 0x2064) return 'math'\n if (fillerSet.has(codepoint)) return 'fillers'\n\n if (codepoint === 0x0020) return undefined\n const char = String.fromCodePoint(codepoint)\n if (isZs.test(char)) return 'spaces'\n if (isCf.test(char)) return 'format'\n if (isCc.test(char)) return 'control'\n\n return undefined\n}\n","import { categorize, charNames, patterns } from './categories'\nimport type { Category, Detection } from './types'\n\nconst formatHex = (cp: number): string => `U+${cp.toString(16).toUpperCase().padStart(4, '0')}`\n\nconst regexCache = new Map<string, RegExp>()\n\nfunction getRegex(categories?: Category[]): RegExp {\n const cats = categories ?? (Object.keys(patterns) as Category[])\n const key = [...cats].sort().join(',')\n let cached = regexCache.get(key)\n if (!cached) {\n cached = new RegExp(cats.map((c) => patterns[c].source).join('|'), 'gu')\n regexCache.set(key, cached)\n }\n return cached\n}\n\n/**\n * Scan a string for invisible Unicode characters and return metadata for each.\n *\n * Optionally pass a list of categories to restrict detection.\n *\n * @example\n * ```ts\n * detect('hello\\u200Bworld')\n * // [{ char: '\\u200B', codepoint: 'U+200B', name: 'ZERO WIDTH SPACE', category: 'format', offset: 5 }]\n *\n * detect('a\\u00a0b\\u200Bc', ['format'])\n * // [{ char: '\\u200B', ..., category: 'format' }]\n * ```\n */\n/**\n * Lazily scan a string for invisible Unicode characters, yielding metadata for each.\n *\n * Like `detect()` but returns a generator — useful for large strings where you\n * may not need all results, or want to break early.\n */\nexport function* scan(input: string, categories?: Category[]): Generator<Detection> {\n const regex = getRegex(categories)\n regex.lastIndex = 0\n\n for (const match of input.matchAll(regex)) {\n const char = match[0]!\n const cp = char.codePointAt(0)!\n const category = categorize(cp)\n\n if (category === undefined) continue\n\n const hex = formatHex(cp)\n yield {\n char,\n codepoint: hex,\n name: charNames[cp] ?? hex,\n category,\n offset: match.index,\n }\n }\n}\n\nexport function detect(input: string, categories?: Category[]): Detection[] {\n return [...scan(input, categories)]\n}\n\n/**\n * Return the first invisible character detected, or `undefined` if clean.\n *\n * Uses `scan()` internally — stops at the first match.\n */\nexport function first(input: string, categories?: Category[]): Detection | undefined {\n for (const d of scan(input, categories)) return d\n return undefined\n}\n\n/**\n * Returns true if the string contains any invisible Unicode characters.\n *\n * Faster than `detect()` when you don't need metadata.\n */\nexport function hasGhosts(input: string, categories?: Category[]): boolean {\n const regex = getRegex(categories)\n regex.lastIndex = 0\n return regex.test(input)\n}\n\n/**\n * Returns true if the string contains no invisible Unicode characters.\n *\n * Inverse of `hasGhosts()`.\n */\nexport function isClean(input: string, categories?: Category[]): boolean {\n return !hasGhosts(input, categories)\n}\n\n/**\n * Count invisible characters by category.\n *\n * @example\n * ```ts\n * count('a\\u00A0b\\u200Bc')\n * // { spaces: 1, format: 1 }\n * ```\n */\nexport function count(input: string, categories?: Category[]): Partial<Record<Category, number>> {\n const result: Partial<Record<Category, number>> = {}\n\n for (const d of detect(input, categories)) {\n result[d.category] = (result[d.category] ?? 0) + 1\n }\n\n return result\n}\n\n/** Result of identifying a single character. */\nexport interface CharInfo {\n codepoint: string\n name: string\n category: Category\n}\n\n/**\n * Identify a single character or codepoint — returns its category and name,\n * or `undefined` if it's not an invisible character deghost tracks.\n *\n * @example\n * ```ts\n * identify('\\u200B')\n * // { codepoint: 'U+200B', name: 'ZERO WIDTH SPACE', category: 'format' }\n *\n * identify(0x00A0)\n * // { codepoint: 'U+00A0', name: 'NO-BREAK SPACE', category: 'spaces' }\n * ```\n */\nexport function identify(input: string | number): CharInfo | undefined {\n const cp = typeof input === 'number' ? input : input.codePointAt(0)!\n const category = categorize(cp)\n\n if (category === undefined) return undefined\n\n const hex = formatHex(cp)\n\n return {\n codepoint: hex,\n name: charNames[cp] ?? hex,\n category,\n }\n}\n","import type { Detection } from './types'\n\n/** Format a detection as `[U+XXXX]`. */\nexport const defaultFormatter = (d: Detection): string => `[${d.codepoint}]`\n\n/** Walk detections and replace each matched character using a mapper. */\nexport function replaceDetections(\n input: string,\n detections: Detection[],\n mapper: (detection: Detection) => string,\n): string {\n if (detections.length === 0) return input\n\n const parts: string[] = []\n let cursor = 0\n\n for (const d of detections) {\n parts.push(input.slice(cursor, d.offset))\n parts.push(mapper(d))\n cursor = d.offset + d.char.length\n }\n\n parts.push(input.slice(cursor))\n\n return parts.join('')\n}\n","import { detect } from './detect'\nimport type { Category } from './types'\n\n/**\n * Return a human-readable report of all invisible characters in a string.\n *\n * @example\n * ```ts\n * summary('hello\\u200Bworld')\n * // \"1 invisible character found.\\n\\nBy category:\\n format: 1\\n\\nDetails:\\n U+200B ZERO WIDTH SPACE (format, offset 5)\"\n * ```\n */\nexport function summary(input: string, categories?: Category[]): string {\n const detections = detect(input, categories)\n\n if (detections.length === 0) return 'No invisible characters found.'\n\n const counts: Partial<Record<Category, number>> = {}\n for (const d of detections) {\n counts[d.category] = (counts[d.category] ?? 0) + 1\n }\n\n const total = detections.length\n const header = `${total} invisible character${total === 1 ? '' : 's'} found.`\n\n const byCategory = Object.entries(counts)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([cat, n]) => ` ${cat}: ${n}`)\n .join('\\n')\n\n const details = detections\n .map((d) => ` ${d.codepoint} ${d.name} (${d.category}, offset ${d.offset})`)\n .join('\\n')\n\n return `${header}\\n\\nBy category:\\n${byCategory}\\n\\nDetails:\\n${details}`\n}\n","import { patterns } from './categories'\nimport * as detection from './detect'\nimport { defaultFormatter, replaceDetections } from './replace'\nimport { summary as summarize } from './summary'\nimport type { Category, Detection } from './types'\n\n/**\n * Fluent chain for composing text cleaning operations.\n *\n * Each method returns a new DeghostChain (immutable), so you can\n * branch chains without side effects.\n *\n * @example\n * ```ts\n * new DeghostChain('text\\u00A0with\\u200Bghosts')\n * .strip('format')\n * .normalize('spaces')\n * .trim()\n * .toString()\n * // 'text with ghosts'\n * ```\n */\nexport class DeghostChain {\n readonly #value: string\n\n constructor(value: string) {\n this.#value = value\n }\n\n /** Remove all characters in the given category. */\n strip(category: Category): DeghostChain {\n return new DeghostChain(this.#value.replace(patterns[category], ''))\n }\n\n /** Replace all characters in the given category with a substitute. */\n normalize(category: Category, replacement: string = ' '): DeghostChain {\n return new DeghostChain(this.#value.replace(patterns[category], replacement))\n }\n\n /** Replace matched ghosts in a category using a mapper function. */\n replace(category: Category, mapper: (detection: Detection) => string): DeghostChain {\n const result = replaceDetections(this.#value, detection.detect(this.#value, [category]), mapper)\n return result === this.#value ? this : new DeghostChain(result)\n }\n\n /** Replace ghosts with visible markers like `[U+200B]`. */\n highlight(\n category?: Category,\n formatter: (detection: Detection) => string = defaultFormatter,\n ): DeghostChain {\n const categories = category ? [category] : undefined\n const result = replaceDetections(\n this.#value,\n detection.detect(this.#value, categories),\n formatter,\n )\n return result === this.#value ? this : new DeghostChain(result)\n }\n\n /** Return detections for the current chain value. */\n detect(categories?: Category[]): Detection[] {\n return detection.detect(this.#value, categories)\n }\n\n /** Check if the current chain value contains invisible characters. */\n hasGhosts(categories?: Category[]): boolean {\n return detection.hasGhosts(this.#value, categories)\n }\n\n /** Count invisible characters by category in the current chain value. */\n count(categories?: Category[]): Partial<Record<Category, number>> {\n return detection.count(this.#value, categories)\n }\n\n /** Returns true if the current chain value has no invisible characters. */\n isClean(categories?: Category[]): boolean {\n return detection.isClean(this.#value, categories)\n }\n\n /** Return a human-readable report of ghosts in the current chain value. */\n summary(categories?: Category[]): string {\n return summarize(this.#value, categories)\n }\n\n /** Collapse runs of whitespace into a single space. */\n collapse(): DeghostChain {\n return new DeghostChain(this.#value.replace(/ {2,}/g, ' '))\n }\n\n /** Trim leading and trailing whitespace. */\n trim(): DeghostChain {\n return new DeghostChain(this.#value.trim())\n }\n\n /** Apply the default cleaning preset: strip format + control, normalize spaces, trim. */\n clean(): DeghostChain {\n return this.strip('format').strip('control').strip('bom').normalize('spaces').collapse().trim()\n }\n\n /** Extract the cleaned string. */\n toString(): string {\n return this.#value\n }\n\n /** Extract the cleaned string (alias for toString). */\n valueOf(): string {\n return this.#value\n }\n\n /** Support JSON.stringify. */\n toJSON(): string {\n return this.#value\n }\n}\n","import { patterns } from './categories'\nimport { detect } from './detect'\nimport { defaultFormatter, replaceDetections } from './replace'\nimport type { Category, CleanerFn, Detection, Rule } from './types'\n\n/**\n * Builder for creating reusable cleaning functions.\n *\n * Compile a cleaning pipeline once, apply it to many strings.\n *\n * @example\n * ```ts\n * const clean = cleaner()\n * .strip('format')\n * .strip('control')\n * .normalize('spaces')\n * .trim()\n * .build()\n *\n * clean('dirty\\u00A0string') // 'dirty string'\n * clean('another\\u200Bone') // 'anotherone'\n * ```\n */\nexport class CleanerBuilder {\n readonly #rules: Rule[] = []\n #trim = false\n #collapse = false\n\n /** Add a strip rule — remove all characters in this category. */\n strip(category: Category): this {\n this.#rules.push({ category, action: 'strip' })\n return this\n }\n\n /** Add a normalize rule — replace characters in this category. */\n normalize(category: Category, replacement: string = ' '): this {\n this.#rules.push({ category, action: 'normalize', replacement })\n return this\n }\n\n /** Add a replace rule — transform characters using detection metadata. */\n replace(category: Category, mapper: (detection: Detection) => string): this {\n this.#rules.push({ category, action: 'replace', mapper })\n return this\n }\n\n /** Add a highlight step — annotate characters with visible markers. */\n highlight(\n category: Category,\n formatter: (detection: Detection) => string = defaultFormatter,\n ): this {\n this.#rules.push({ category, action: 'replace', mapper: formatter })\n return this\n }\n\n /** Enable whitespace trimming as a final step. */\n trim(): this {\n this.#trim = true\n return this\n }\n\n /** Enable collapsing runs of whitespace as a final step. */\n collapse(): this {\n this.#collapse = true\n return this\n }\n\n /** Compile the pipeline into a reusable function. */\n build(): CleanerFn {\n const steps = this.#rules.map((rule) => {\n if (rule.action === 'replace') {\n const { mapper, category } = rule\n return (s: string) => replaceDetections(s, detect(s, [category]), mapper)\n }\n const pattern = patterns[rule.category]\n if (rule.action === 'strip') {\n return (s: string) => s.replace(pattern, '')\n }\n const replacement = rule.replacement ?? ' '\n return (s: string) => s.replace(pattern, replacement)\n })\n\n const doCollapse = this.#collapse\n const doTrim = this.#trim\n\n return (input: string): string => {\n let result = input\n for (const step of steps) {\n result = step(result)\n }\n if (doCollapse) result = result.replace(/ {2,}/g, ' ')\n if (doTrim) result = result.trim()\n return result\n }\n }\n}\n\n/** Create a new cleaner builder. */\nexport function cleaner(): CleanerBuilder {\n return new CleanerBuilder()\n}\n","import { cleaner } from './cleaner'\nimport type { CleanerFn } from './types'\n\n/**\n * Pre-built cleaning functions for common use cases.\n *\n * @example\n * ```ts\n * import { presets } from 'deghost'\n *\n * presets.clean('text\\u00A0with\\u200Bghosts')\n * // 'text with ghosts'\n * ```\n */\nexport const presets = {\n /**\n * Default clean: strip format + control + BOM, normalize spaces, collapse, trim.\n *\n * The right choice for most text processing — catches invisible chars from\n * binary formats (Garmin FIT, PDFs), APIs, and copy-paste while preserving\n * word boundaries.\n */\n clean: cleaner()\n .strip('format')\n .strip('control')\n .strip('bom')\n .normalize('spaces')\n .collapse()\n .trim()\n .build(),\n\n /**\n * Aggressive: strip everything invisible, including fillers, math operators, and tags.\n *\n * Use when you want maximally clean output and don't need to preserve any\n * invisible Unicode semantics (ligature joiners, bidi marks, etc.).\n */\n aggressive: cleaner()\n .strip('format')\n .strip('control')\n .strip('bom')\n .strip('tag')\n .strip('fillers')\n .strip('math')\n .normalize('spaces')\n .collapse()\n .trim()\n .build(),\n\n /**\n * Spaces only: normalize Unicode whitespace to ASCII space.\n *\n * Leaves format/control characters alone. Useful when you only care about\n * NBSP and exotic spaces (common in data from Garmin, Strava, etc.).\n */\n spaces: cleaner().normalize('spaces').collapse().trim().build(),\n} satisfies Record<string, CleanerFn>\n","import { detect } from './detect'\nimport { defaultFormatter, replaceDetections } from './replace'\nimport type { Detection, HighlightOptions } from './types'\n\n/**\n * Replace invisible characters with visible markers for debugging.\n *\n * By default each ghost is replaced with its codepoint in brackets,\n * e.g. `[U+200B]`. Pass a custom formatter or an options object to\n * filter by category.\n *\n * @example\n * ```ts\n * highlight('hello\\u200Bworld')\n * // 'hello[U+200B]world'\n *\n * highlight('a\\u200Bb', (d) => `{${d.name}}`)\n * // 'a{ZERO WIDTH SPACE}b'\n *\n * highlight('a\\u00a0b\\u200Bc', { categories: ['format'] })\n * // 'a\\u00a0b[U+200B]c'\n * ```\n */\nexport function highlight(input: string): string\nexport function highlight(input: string, formatter: (detection: Detection) => string): string\nexport function highlight(input: string, options: HighlightOptions): string\nexport function highlight(\n input: string,\n options?: ((detection: Detection) => string) | HighlightOptions,\n): string {\n const formatter =\n typeof options === 'function' ? options : (options?.formatter ?? defaultFormatter)\n const categories = typeof options === 'object' ? options.categories : undefined\n\n return replaceDetections(input, detect(input, categories), formatter)\n}\n","import { DeghostChain } from './chain'\nimport type { DeghostOptions } from './types'\nimport { presets } from './presets'\n\nexport { DeghostChain } from './chain'\nexport { CleanerBuilder, cleaner } from './cleaner'\nexport { scan, detect, first, hasGhosts, isClean, count, identify } from './detect'\nexport { highlight } from './highlight'\nexport { summary } from './summary'\nexport { presets } from './presets'\nexport { patterns, descriptions, charNames, categories, categorize } from './categories'\n\nexport type {\n Category,\n Action,\n Detection,\n HighlightOptions,\n Rule,\n CleanerFn,\n DeghostOptions,\n} from './types'\nexport type { CharInfo } from './detect'\n\n/**\n * Clean a string of invisible Unicode characters.\n *\n * With no chaining, applies the default preset (strip format + control + BOM,\n * normalize spaces, collapse, trim). Chain methods for fine-grained control.\n *\n * @example\n * ```ts\n * // Quick clean — sensible defaults\n * deghost('Plant\\u00a064\\u00a0-\\u00a0Woodbridge')\n * // → 'Plant 64 - Woodbridge'\n *\n * // Tagged template literal\n * deghost`text\\u200B\\u00a0here`\n * // → 'text here'\n *\n * // Chainable — pick what to handle\n * deghost('text\\u200B\\u00a0here')\n * .strip('format')\n * .normalize('spaces')\n * .trim()\n * .toString()\n * ```\n */\nexport function deghost(strings: TemplateStringsArray, ...values: unknown[]): DeghostChain & string\nexport function deghost(input: string, options?: DeghostOptions): DeghostChain & string\nexport function deghost(\n input: string | TemplateStringsArray,\n ...rest: unknown[]\n): DeghostChain & string {\n let raw: string\n if (typeof input === 'string') {\n raw = input\n } else {\n raw = input[0] ?? ''\n for (let i = 0; i < rest.length; i++) {\n raw += String(rest[i]) + (input[i + 1] ?? '')\n }\n }\n\n const options =\n typeof input === 'string' && rest.length <= 1\n ? (rest[0] as DeghostOptions | undefined)\n : undefined\n const chain = new DeghostChain(raw)\n const cleaned = presets.clean(raw)\n const trimmed = (options?.trim ?? true) ? cleaned.trim() : cleaned\n\n // Return a chain that also coerces to the default-cleaned string\n return new Proxy(chain, {\n get(target, prop, receiver) {\n if (prop === Symbol.toPrimitive) return () => trimmed\n if (prop === 'length') return trimmed.length\n return Reflect.get(target, prop, receiver)\n },\n }) as DeghostChain & string\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "deghost",
3
+ "version": "0.0.1",
4
+ "description": "Strip invisible Unicode characters and normalize whitespace. Chainable, typesafe, zero dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "tsc --noEmit",
31
+ "format": "prettier --write .",
32
+ "format:check": "prettier --check .",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "keywords": [
36
+ "unicode",
37
+ "invisible",
38
+ "zero-width",
39
+ "nbsp",
40
+ "text",
41
+ "normalization",
42
+ "sanitize",
43
+ "strip",
44
+ "clean",
45
+ "whitespace",
46
+ "control-characters"
47
+ ],
48
+ "author": "Kelly Mears",
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/kellymears/deghost.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/kellymears/deghost/issues"
56
+ },
57
+ "homepage": "https://github.com/kellymears/deghost#readme",
58
+ "devDependencies": {
59
+ "prettier": "^3.0.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.7.0",
62
+ "vitest": "^3.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=18"
66
+ }
67
+ }