zero-contact 0.1.0

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 ADDED
@@ -0,0 +1,108 @@
1
+ # zero-contact
2
+
3
+ Detect obfuscated phone numbers in user-generated text. Built for moderation pipelines where contact sharing is restricted.
4
+
5
+ Supports numeric, word-spelled, emoji, homoglyph, and mixed obfuscation formats in Turkish and English.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install zero-contact
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { detect, mask, quickCheck } from 'zero-contact';
17
+
18
+ quickCheck('hello world'); // false
19
+
20
+ detect('0532 123 45 67');
21
+ // { detected: true, matches: [{ normalized: '05321234567', confidence: 0.9, ... }] }
22
+
23
+ detect('beş üç iki bir iki üç dört beş altı yedi');
24
+ // { detected: true, matches: [{ normalized: '5321234567', types: ['word'], ... }] }
25
+
26
+ mask('Arayın: 0532 123 45 67');
27
+ // 'Arayın: #### ### ## ##'
28
+ ```
29
+
30
+ ## API
31
+
32
+ ### `quickCheck(text, options?)`
33
+
34
+ Fast pre-scan. Returns `true` when the text may contain a phone number. Use this as a cheap gate before `detect()`.
35
+
36
+ ### `detect(text, options?)`
37
+
38
+ Full pipeline. Returns `{ detected, matches }` with confidence scores and source ranges.
39
+
40
+ ### `mask(text, options?)`
41
+
42
+ Detects and masks phone-like sequences while preserving string length. Spaces are kept; other characters are replaced with `#` by default.
43
+
44
+ ### Options
45
+
46
+ | Option | Default | Description |
47
+ |---|---|---|
48
+ | `locales` | `[tr, en]` | Locale dictionaries for word-digit matching |
49
+ | `minDigits` | `7` | Minimum consecutive digits to flag |
50
+ | `minConfidence` | `0.6` | Minimum confidence score (0–1) |
51
+ | `countryHint` | `'TR'` | Scoring hint for national formats |
52
+ | `char` | `'#'` | Mask character (`mask` only) |
53
+
54
+ ## Tree-shakeable locales
55
+
56
+ Import only the locales you need:
57
+
58
+ ```ts
59
+ import { detect } from 'zero-contact';
60
+ import { tr } from 'zero-contact/locales/tr';
61
+
62
+ detect('beş üç iki bir iki üç dört beş altı yedi', { locales: [tr] });
63
+ ```
64
+
65
+ ## What it detects
66
+
67
+ - Plain numbers: `0532 123 45 67`, `+90 532 123 45 67`
68
+ - Unicode digits: `٠٥٣٢ ١٢٣ ٤٥ ٦٧`
69
+ - Word spelling: `five three two...`, `beş üç iki...`
70
+ - Emoji keycaps: `5️⃣3️⃣2️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣`
71
+ - Circled digits: `⑤③②①②③④⑤⑥⑦`
72
+ - Homoglyphs: `O5³2 l23 4567`
73
+ - Mixed obfuscation: `beş 5 üç iki bir...`
74
+ - Multipliers: `double five` → `55`, `çift bir` → `11`
75
+
76
+ ## False-positive guards
77
+
78
+ Filters out common non-phone patterns:
79
+
80
+ - Dates: `01/02/2024`
81
+ - Times: `14:30`
82
+ - IPv4 addresses: `192.168.1.1`
83
+ - Isolated digit words in prose: `bu ürün beş yıldız`
84
+
85
+ ## Performance
86
+
87
+ Benchmarks on a typical dev machine:
88
+
89
+ | Scenario | ~Mean |
90
+ |---|---|
91
+ | `quickCheck` on clean 200-char comment | 0.017 ms |
92
+ | `detect` on obfuscated phone | 0.043 ms |
93
+ | `detect` on 2 KB text | 1.4 ms |
94
+
95
+ Zero runtime dependencies.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ npm install
101
+ npm test
102
+ npm run bench
103
+ npm run build
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,572 @@
1
+ 'use strict';
2
+
3
+ // src/match/false-positives.ts
4
+ var DATE_PATTERNS = [
5
+ /\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b/,
6
+ /\b\d{4}[/.-]\d{1,2}[/.-]\d{1,2}\b/
7
+ ];
8
+ var TIME_PATTERNS = [
9
+ /\b\d{1,2}:\d{2}(?::\d{2})?\b/,
10
+ /\b\d{1,2}\.\d{2}\b/
11
+ ];
12
+ var IPV4_PATTERN = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
13
+ function isFalsePositive(sequence, sourceText) {
14
+ const snippet = sourceText.slice(sequence.start, sequence.end);
15
+ for (const pattern of DATE_PATTERNS) {
16
+ if (pattern.test(snippet)) {
17
+ return true;
18
+ }
19
+ }
20
+ for (const pattern of TIME_PATTERNS) {
21
+ if (pattern.test(snippet)) {
22
+ return true;
23
+ }
24
+ }
25
+ if (IPV4_PATTERN.test(snippet)) {
26
+ return true;
27
+ }
28
+ if (/^(\d)\1{11,}$/.test(sequence.normalized)) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ // src/match/scorer.ts
35
+ function isTrMobile(normalized) {
36
+ const digits = normalized.replace(/\D/g, "");
37
+ if (digits.length === 10 && /^5\d{9}$/.test(digits)) {
38
+ return true;
39
+ }
40
+ if (digits.length === 11 && /^05\d{9}$/.test(digits)) {
41
+ return true;
42
+ }
43
+ if (digits.length === 12 && /^905\d{9}$/.test(digits)) {
44
+ return true;
45
+ }
46
+ if (digits.length === 13 && /^\+?905\d{9}$/.test(digits)) {
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ function hasPrefix(sourceText, sequence) {
52
+ const before = sourceText.slice(Math.max(0, sequence.start - 4), sequence.start);
53
+ return /\+/.test(before) || /^0/.test(sequence.normalized);
54
+ }
55
+ function countObfuscationTypes(types) {
56
+ if (types.includes("mixed")) {
57
+ return 3;
58
+ }
59
+ return types.length;
60
+ }
61
+ function scoreSequence(sequence, sourceText, options) {
62
+ let score = 0.35;
63
+ const digits = sequence.normalized;
64
+ const digitCount = digits.length;
65
+ const countryHint = options.countryHint ?? "TR";
66
+ if (countryHint === "TR" && isTrMobile(digits)) {
67
+ score += 0.4;
68
+ } else if (digitCount >= 10 && digitCount <= 15) {
69
+ score += 0.25;
70
+ } else if (digitCount >= 7) {
71
+ score += 0.15;
72
+ }
73
+ if (hasPrefix(sourceText, sequence)) {
74
+ score += 0.15;
75
+ }
76
+ const typeCount = countObfuscationTypes(sequence.types);
77
+ if (typeCount >= 2 || sequence.types.includes("mixed")) {
78
+ score += 0.2;
79
+ } else if (sequence.types[0] !== "numeric") {
80
+ score += 0.15;
81
+ }
82
+ if (sequence.types.length === 1 && sequence.types[0] === "numeric" && digitCount === 10) {
83
+ score += 0.1;
84
+ }
85
+ if (digitCount < 8) {
86
+ score -= 0.15;
87
+ }
88
+ return Math.max(0, Math.min(1, score));
89
+ }
90
+
91
+ // src/locales/types.ts
92
+ var SEPARATORS = /* @__PURE__ */ new Set([" ", " ", "-", ".", ",", "/", "(", ")", "+", "\xA0"]);
93
+ var DEFAULT_MIN_DIGITS = 7;
94
+ var DEFAULT_MIN_CONFIDENCE = 0.6;
95
+
96
+ // src/normalize/emoji.ts
97
+ var CIRCLED_DIGITS = {
98
+ 9450: "0",
99
+ 9312: "1",
100
+ 9313: "2",
101
+ 9314: "3",
102
+ 9315: "4",
103
+ 9316: "5",
104
+ 9317: "6",
105
+ 9318: "7",
106
+ 9319: "8",
107
+ 9320: "9",
108
+ 10102: "1",
109
+ 10103: "2",
110
+ 10104: "3",
111
+ 10105: "4",
112
+ 10106: "5",
113
+ 10107: "6",
114
+ 10108: "7",
115
+ 10109: "8",
116
+ 10110: "9"
117
+ };
118
+ function isKeycapStart(text, index) {
119
+ const char = text[index];
120
+ if (!char || !/[0-9#*]/.test(char)) return null;
121
+ let consumed = 1;
122
+ if (text[index + 1] === "\uFE0F") {
123
+ consumed += 1;
124
+ }
125
+ if (text[index + consumed] === "\u20E3") {
126
+ consumed += 1;
127
+ if (char === "#" || char === "*") return null;
128
+ return consumed;
129
+ }
130
+ return null;
131
+ }
132
+ function circledDigitToAscii(text, index) {
133
+ const code = text.codePointAt(index);
134
+ if (code === void 0) return null;
135
+ const digit = CIRCLED_DIGITS[code];
136
+ if (!digit) return null;
137
+ const length = code > 65535 ? 2 : 1;
138
+ return { digit, length };
139
+ }
140
+ var EMOJI_PRESCAN = /(?:[0-9#*]\uFE0F?\u20E3|[\u2460-\u2468\u24EA\u2776-\u277E])/u;
141
+
142
+ // src/normalize/homoglyph.ts
143
+ var HOMOGLYPH_MAP = {
144
+ O: "0",
145
+ o: "0",
146
+ l: "1",
147
+ I: "1",
148
+ i: "1",
149
+ "|": "1",
150
+ "!": "1",
151
+ S: "5",
152
+ s: "5",
153
+ B: "8",
154
+ Z: "2",
155
+ z: "2",
156
+ g: "9",
157
+ q: "9"
158
+ };
159
+ function homoglyphToDigit(char) {
160
+ return HOMOGLYPH_MAP[char] ?? null;
161
+ }
162
+ function isDigitLikeContext(text, index) {
163
+ const prev = text[index - 1];
164
+ const next = text[index + 1];
165
+ const prevIsDigitLike = prev !== void 0 && (/[0-9]/.test(prev) || homoglyphToDigit(prev) !== null || /[\s\-.,/()+]/.test(prev));
166
+ const nextIsDigitLike = next !== void 0 && (/[0-9]/.test(next) || homoglyphToDigit(next) !== null || /[\s\-.,/()+]/.test(next));
167
+ return prevIsDigitLike || nextIsDigitLike;
168
+ }
169
+
170
+ // src/normalize/unicode.ts
171
+ var INVISIBLE_CHARS = /[\u200B-\u200D\uFEFF\u2060\u180E]/g;
172
+ function nfkc(text) {
173
+ return text.normalize("NFKC");
174
+ }
175
+ function stripInvisible(text) {
176
+ return text.replace(INVISIBLE_CHARS, "");
177
+ }
178
+ function unicodeDigitToAscii(char) {
179
+ const code = char.codePointAt(0);
180
+ if (code === void 0) return null;
181
+ if (code >= 48 && code <= 57) {
182
+ return char;
183
+ }
184
+ if (code >= 65296 && code <= 65305) {
185
+ return String.fromCharCode(code - 65296 + 48);
186
+ }
187
+ if (code >= 1632 && code <= 1641) {
188
+ return String.fromCharCode(code - 1632 + 48);
189
+ }
190
+ if (code >= 1776 && code <= 1785) {
191
+ return String.fromCharCode(code - 1776 + 48);
192
+ }
193
+ if (code >= 2406 && code <= 2415) {
194
+ return String.fromCharCode(code - 2406 + 48);
195
+ }
196
+ const superscriptMap = {
197
+ 8304: "0",
198
+ 185: "1",
199
+ 178: "2",
200
+ 179: "3",
201
+ 8308: "4",
202
+ 8309: "5",
203
+ 8310: "6",
204
+ 8311: "7",
205
+ 8312: "8",
206
+ 8313: "9"
207
+ };
208
+ const superscript = superscriptMap[code];
209
+ if (superscript) {
210
+ return superscript;
211
+ }
212
+ return null;
213
+ }
214
+ function foldTurkish(text) {
215
+ return text.replace(/ı/g, "i").replace(/İ/g, "i").replace(/ş/g, "s").replace(/Ş/g, "s").replace(/ğ/g, "g").replace(/Ğ/g, "g").replace(/ü/g, "u").replace(/Ü/g, "u").replace(/ö/g, "o").replace(/Ö/g, "o").replace(/ç/g, "c").replace(/Ç/g, "c");
216
+ }
217
+ function normalizeForLookup(text) {
218
+ return foldTurkish(nfkc(stripInvisible(text)).toLowerCase());
219
+ }
220
+
221
+ // src/normalize/word-digits.ts
222
+ function buildLookup(locales) {
223
+ const digitWords = /* @__PURE__ */ new Map();
224
+ const multipliers = /* @__PURE__ */ new Map();
225
+ for (const locale of locales) {
226
+ for (const [word, digit] of locale.digitWords) {
227
+ digitWords.set(normalizeForLookup(word), digit);
228
+ }
229
+ for (const [word, count] of locale.multipliers) {
230
+ multipliers.set(normalizeForLookup(word), count);
231
+ }
232
+ }
233
+ const words = [.../* @__PURE__ */ new Set([...digitWords.keys(), ...multipliers.keys()])].sort((a, b) => b.length - a.length).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
234
+ const wordPattern = new RegExp(`\\b(?:${words.join("|")})\\b`, "giu");
235
+ return { digitWords, multipliers, wordPattern };
236
+ }
237
+ function isSeparator(char) {
238
+ return /[\s\-.,/()+]/.test(char) || char === "\xA0";
239
+ }
240
+ function isWordChar(char) {
241
+ return /[\p{L}_]/u.test(char);
242
+ }
243
+ function pushDigit(tokens, digit, start, end, type, repeat = 1) {
244
+ for (let i = 0; i < repeat; i += 1) {
245
+ tokens.push({ digit, start, end, type });
246
+ }
247
+ }
248
+ function tokenizeDigits(text, locales) {
249
+ const lookup = buildLookup(locales);
250
+ const tokens = [];
251
+ let pendingRepeat = 1;
252
+ let index = 0;
253
+ while (index < text.length) {
254
+ const keycapLength = isKeycapStart(text, index);
255
+ if (keycapLength !== null) {
256
+ const digit = text[index];
257
+ pushDigit(tokens, digit, index, index + keycapLength, "emoji", pendingRepeat);
258
+ pendingRepeat = 1;
259
+ index += keycapLength;
260
+ continue;
261
+ }
262
+ const circled = circledDigitToAscii(text, index);
263
+ if (circled) {
264
+ pushDigit(tokens, circled.digit, index, index + circled.length, "emoji", pendingRepeat);
265
+ pendingRepeat = 1;
266
+ index += circled.length;
267
+ continue;
268
+ }
269
+ const char = text[index];
270
+ if (isWordChar(char)) {
271
+ let end = index + 1;
272
+ while (end < text.length && isWordChar(text[end])) {
273
+ end += 1;
274
+ }
275
+ if (end === index + 1) {
276
+ const homoglyph2 = homoglyphToDigit(char);
277
+ if (homoglyph2 && isDigitLikeContext(text, index)) {
278
+ pushDigit(tokens, homoglyph2, index, index + 1, "homoglyph", pendingRepeat);
279
+ pendingRepeat = 1;
280
+ index += 1;
281
+ continue;
282
+ }
283
+ }
284
+ const rawWord = text.slice(index, end);
285
+ const normalizedWord = normalizeForLookup(rawWord);
286
+ const multiplier = lookup.multipliers.get(normalizedWord);
287
+ if (multiplier !== void 0) {
288
+ pendingRepeat = multiplier;
289
+ index = end;
290
+ continue;
291
+ }
292
+ const digit = lookup.digitWords.get(normalizedWord);
293
+ if (digit !== void 0) {
294
+ pushDigit(tokens, digit, index, end, "word", pendingRepeat);
295
+ pendingRepeat = 1;
296
+ index = end;
297
+ continue;
298
+ }
299
+ index = end;
300
+ continue;
301
+ }
302
+ const asciiDigit = unicodeDigitToAscii(char);
303
+ if (asciiDigit) {
304
+ pushDigit(tokens, asciiDigit, index, index + 1, "numeric", pendingRepeat);
305
+ pendingRepeat = 1;
306
+ index += 1;
307
+ continue;
308
+ }
309
+ const homoglyph = homoglyphToDigit(char);
310
+ if (homoglyph && isDigitLikeContext(text, index)) {
311
+ pushDigit(tokens, homoglyph, index, index + 1, "homoglyph", pendingRepeat);
312
+ pendingRepeat = 1;
313
+ index += 1;
314
+ continue;
315
+ }
316
+ if (isSeparator(char)) {
317
+ index += 1;
318
+ continue;
319
+ }
320
+ pendingRepeat = 1;
321
+ index += 1;
322
+ }
323
+ return tokens;
324
+ }
325
+ function hasLocaleWordSignal(text, locales) {
326
+ const lookup = buildLookup(locales);
327
+ const pattern = lookup.wordPattern;
328
+ pattern.lastIndex = 0;
329
+ return pattern.test(text);
330
+ }
331
+ function isMultiplierSpan(text, from, to, locales) {
332
+ const slice = text.slice(from, to).trim();
333
+ if (!slice) {
334
+ return true;
335
+ }
336
+ const lookup = buildLookup(locales);
337
+ const words = slice.split(/\s+/);
338
+ return words.every((word) => lookup.multipliers.has(normalizeForLookup(word)));
339
+ }
340
+
341
+ // src/match/sequence.ts
342
+ function onlySeparatorsBetween(text, from, to, locales) {
343
+ if (from >= to) {
344
+ return true;
345
+ }
346
+ if (isMultiplierSpan(text, from, to, locales)) {
347
+ return true;
348
+ }
349
+ for (let i = from; i < to; i += 1) {
350
+ const char = text[i];
351
+ if (!SEPARATORS.has(char)) {
352
+ return false;
353
+ }
354
+ }
355
+ return true;
356
+ }
357
+ function collectTypes(tokens) {
358
+ const unique = new Set(tokens.map((token) => token.type));
359
+ if (unique.size > 1) {
360
+ return ["mixed"];
361
+ }
362
+ return [...unique];
363
+ }
364
+ function findDigitSequences(text, tokens, minDigits, locales = []) {
365
+ if (tokens.length === 0) {
366
+ return [];
367
+ }
368
+ const sequences = [];
369
+ let group = [tokens[0]];
370
+ for (let i = 1; i < tokens.length; i += 1) {
371
+ const prev = tokens[i - 1];
372
+ const current = tokens[i];
373
+ if (onlySeparatorsBetween(text, prev.end, current.start, locales)) {
374
+ group.push(current);
375
+ continue;
376
+ }
377
+ if (group.length >= minDigits) {
378
+ sequences.push(toSequence(group));
379
+ }
380
+ group = [current];
381
+ }
382
+ if (group.length >= minDigits) {
383
+ sequences.push(toSequence(group));
384
+ }
385
+ return sequences;
386
+ }
387
+ function toSequence(tokens) {
388
+ const first = tokens[0];
389
+ const last = tokens[tokens.length - 1];
390
+ return {
391
+ tokens,
392
+ start: first.start,
393
+ end: last.end,
394
+ normalized: tokens.map((token) => token.digit).join(""),
395
+ types: collectTypes(tokens)
396
+ };
397
+ }
398
+
399
+ // src/locales/build-locale.ts
400
+ function createLocale(id, digitWords, multipliers = {}) {
401
+ const digitMap = new Map(Object.entries(digitWords));
402
+ const multiplierMap = new Map(Object.entries(multipliers));
403
+ const words = [...digitMap.keys(), ...multiplierMap.keys()].sort((a, b) => b.length - a.length).map(escapeRegex);
404
+ const preScanPattern = new RegExp(
405
+ `(?:${words.join("|")})`,
406
+ "iu"
407
+ );
408
+ return {
409
+ id,
410
+ digitWords: digitMap,
411
+ multipliers: multiplierMap,
412
+ preScanPattern
413
+ };
414
+ }
415
+ function escapeRegex(value) {
416
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
417
+ }
418
+
419
+ // src/locales/en.ts
420
+ var en = createLocale(
421
+ "en",
422
+ {
423
+ zero: "0",
424
+ oh: "0",
425
+ one: "1",
426
+ two: "2",
427
+ three: "3",
428
+ four: "4",
429
+ five: "5",
430
+ six: "6",
431
+ seven: "7",
432
+ eight: "8",
433
+ nine: "9"
434
+ },
435
+ {
436
+ double: 2,
437
+ triple: 3,
438
+ quad: 4
439
+ }
440
+ );
441
+
442
+ // src/locales/tr.ts
443
+ var tr = createLocale(
444
+ "tr",
445
+ {
446
+ sifir: "0",
447
+ s\u0131f\u0131r: "0",
448
+ bir: "1",
449
+ iki: "2",
450
+ uc: "3",
451
+ \u00FC\u00E7: "3",
452
+ dort: "4",
453
+ d\u00F6rt: "4",
454
+ bes: "5",
455
+ be\u015F: "5",
456
+ alti: "6",
457
+ alt\u0131: "6",
458
+ yedi: "7",
459
+ sekiz: "8",
460
+ dokuz: "9"
461
+ },
462
+ {
463
+ cift: 2,
464
+ \u00E7ift: 2,
465
+ uclu: 3,
466
+ \u00FC\u00E7l\u00FC: 3,
467
+ dortlu: 4,
468
+ d\u00F6rtl\u00FC: 4
469
+ }
470
+ );
471
+
472
+ // src/pipeline/options.ts
473
+ function resolveOptions(options) {
474
+ return {
475
+ locales: options?.locales ?? [tr, en],
476
+ minDigits: options?.minDigits ?? DEFAULT_MIN_DIGITS,
477
+ minConfidence: options?.minConfidence ?? DEFAULT_MIN_CONFIDENCE,
478
+ countryHint: options?.countryHint ?? "TR"
479
+ };
480
+ }
481
+
482
+ // src/pipeline/quick-check.ts
483
+ function quickCheck(text, options) {
484
+ if (!text || text.length === 0) {
485
+ return false;
486
+ }
487
+ const resolved = resolveOptions(options);
488
+ if (/\d/.test(text)) {
489
+ return true;
490
+ }
491
+ if (/\p{Nd}/u.test(text)) {
492
+ return true;
493
+ }
494
+ if (EMOJI_PRESCAN.test(text)) {
495
+ return true;
496
+ }
497
+ if (hasLocaleWordSignal(text, resolved.locales)) {
498
+ return true;
499
+ }
500
+ for (const locale of resolved.locales) {
501
+ locale.preScanPattern.lastIndex = 0;
502
+ if (locale.preScanPattern.test(text)) {
503
+ return true;
504
+ }
505
+ }
506
+ if (/[+]\s*[\d\p{Nd}\s\-().]{4,}/u.test(text)) {
507
+ return true;
508
+ }
509
+ if (/(?:[\d\p{Nd}][\s.\-/)]{0,3}){3,}[\d\p{Nd}]/u.test(text)) {
510
+ return true;
511
+ }
512
+ return false;
513
+ }
514
+
515
+ // src/pipeline/detect.ts
516
+ function detect(text, options) {
517
+ if (!quickCheck(text, options)) {
518
+ return { detected: false, matches: [] };
519
+ }
520
+ const resolved = resolveOptions(options);
521
+ const tokens = tokenizeDigits(text, resolved.locales);
522
+ const sequences = findDigitSequences(text, tokens, resolved.minDigits, resolved.locales);
523
+ const matches = [];
524
+ for (const sequence of sequences) {
525
+ if (isFalsePositive(sequence, text)) {
526
+ continue;
527
+ }
528
+ const confidence = scoreSequence(sequence, text, resolved);
529
+ if (confidence < resolved.minConfidence) {
530
+ continue;
531
+ }
532
+ matches.push({
533
+ start: sequence.start,
534
+ end: sequence.end,
535
+ text: text.slice(sequence.start, sequence.end),
536
+ normalized: sequence.normalized,
537
+ confidence,
538
+ types: sequence.types
539
+ });
540
+ }
541
+ return {
542
+ detected: matches.length > 0,
543
+ matches
544
+ };
545
+ }
546
+
547
+ // src/pipeline/mask.ts
548
+ function mask(text, options) {
549
+ const result = detect(text, options);
550
+ if (!result.detected) {
551
+ return text;
552
+ }
553
+ const char = options?.char ?? "#";
554
+ const chars = [...text];
555
+ const sorted = [...result.matches].sort((a, b) => b.start - a.start);
556
+ for (const match of sorted) {
557
+ for (let i = match.start; i < match.end; i += 1) {
558
+ if (!/\s/.test(chars[i] ?? "")) {
559
+ chars[i] = char;
560
+ }
561
+ }
562
+ }
563
+ return chars.join("");
564
+ }
565
+
566
+ exports.detect = detect;
567
+ exports.en = en;
568
+ exports.mask = mask;
569
+ exports.quickCheck = quickCheck;
570
+ exports.tr = tr;
571
+ //# sourceMappingURL=index.cjs.map
572
+ //# sourceMappingURL=index.cjs.map