maskarajs 1.0.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/mask.cjs.js ADDED
@@ -0,0 +1,874 @@
1
+ /**
2
+ * mask.js — engine de máscaras com padrão declarativo
3
+ *
4
+ * Sintaxe do padrão:
5
+ * # → qualquer dígito (0–9)
6
+ * @ → qualquer letra (a–z, A–Z, acentuados)
7
+ * * → qualquer caractere
8
+ * [texto] → literal fixo (inserido/removido automaticamente)
9
+ * {expr} → slot livre: testa UM caractere contra a expressão
10
+ *
11
+ * Sintaxe de {expr} — três formas, resolvidas nesta ordem:
12
+ * 1. Regex explícita — contém \, [, ^, (, | → new RegExp(expr)
13
+ * {\d} → /\d/.test(ch)
14
+ * {[0-4]} → /[0-4]/.test(ch)
15
+ * {[^aeiou]} → qualquer consoante
16
+ * 2. Intervalo — exatamente "x-y" (3 chars com hífen no meio)
17
+ * {0-4} → ch >= '0' && ch <= '4'
18
+ * {a-f} → ch >= 'a' && ch <= 'f'
19
+ * 3. Conjunto literal — qualquer outra coisa
20
+ * {4} → só o char '4'
21
+ * {013} → '0', '1' ou '3'
22
+ * {SP} → 'S' ou 'P'
23
+ *
24
+ * Exemplos reais:
25
+ * CEP: '#####[-]###'
26
+ * Telefone: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####']
27
+ * CPF: '###[.]###[.]###[-]##'
28
+ * CNPJ: '##[.]###[.]###[/]####[-]##'
29
+ * Data: '##[/]##[/]####'
30
+ * Mês: '{0-1}#' + validate incremental para aceitar só 01–12
31
+ * Cartão: '#### #### #### ####'
32
+ * Visa: '{4}### #### #### ####'
33
+ * Mês válido: '{0-1}{0-9}[/]{0-3}{0-9}[/]####'
34
+ * Hex color: '{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}{[0-9a-fA-F]}'
35
+ */
36
+
37
+ // ─── Cache do parser ───────────────────────────────────────────────────────
38
+ //
39
+ // parse() compila regex e aloca tokens a cada chamada.
40
+ // Como o mesmo padrão é usado em extractInputChars + applyTokens + inputCount
41
+ // a cada keystroke, cachear elimina recompilações redundantes.
42
+ //
43
+ // Chave: string do padrão → Valor: array de tokens imutáveis
44
+
45
+ const _parseCache = new Map()
46
+
47
+ // ─── Parser de padrão ──────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Resolve {expr} para um predicado de teste (ch: string) => boolean.
51
+ *
52
+ * Ordem de resolução:
53
+ * 1. Regex explícita — expr contém \ [ ^ ( |
54
+ * 2. Intervalo — exatamente "x-y"
55
+ * 3. Conjunto literal
56
+ */
57
+ function resolveExpr(expr) {
58
+ const isRegex = /[\\[^(|]/.test(expr) || expr.startsWith('\\')
59
+ if (isRegex) {
60
+ let re
61
+ try { re = new RegExp(expr) } catch (e) {
62
+ throw new Error(`mask: expressão inválida "{${expr}}" — ${e.message}`)
63
+ }
64
+ return { test: ch => re.test(ch), constraint: expr }
65
+ }
66
+ if (expr.length === 3 && expr[1] === '-') {
67
+ const lo = expr[0], hi = expr[2]
68
+ return { test: ch => ch >= lo && ch <= hi, constraint: expr }
69
+ }
70
+ return { test: ch => expr.includes(ch), constraint: expr }
71
+ }
72
+
73
+ /** Predicados padrão para os tokens base sem modificador */
74
+ const DEFAULT_SLOTS = Object.freeze({
75
+ '#': { test: ch => /\d/.test(ch), hint: '0' },
76
+ '@': { test: ch => /[A-Za-zÀ-ÿ]/.test(ch), hint: 'A' },
77
+ '*': { test: () => true, hint: '_' },
78
+ })
79
+
80
+ const globalSlots = { ...DEFAULT_SLOTS }
81
+ let globalSlotsVersion = 0
82
+ let slotLanguageSeq = 0
83
+
84
+ function normalizeSlot(symbol, definition) {
85
+ if (typeof symbol !== 'string' || Array.from(symbol).length !== 1) {
86
+ throw new Error('mask.defineSlot: o símbolo precisa ter exatamente 1 caractere')
87
+ }
88
+ if (symbol === '[' || symbol === ']' || symbol === '{' || symbol === '}') {
89
+ throw new Error(`mask.defineSlot: "${symbol}" é reservado pela sintaxe de pattern`)
90
+ }
91
+
92
+ if (typeof definition === 'function') {
93
+ return { test: definition, hint: symbol }
94
+ }
95
+
96
+ if (definition instanceof RegExp) {
97
+ const flags = definition.flags.replace(/[gy]/g, '')
98
+ const re = new RegExp(definition.source, flags)
99
+ return { test: ch => re.test(ch), hint: symbol }
100
+ }
101
+
102
+ if (!definition || typeof definition.test !== 'function') {
103
+ throw new Error(`mask.defineSlot: "${symbol}" precisa de uma função, RegExp ou { test, hint }`)
104
+ }
105
+
106
+ return {
107
+ test: definition.test,
108
+ hint: typeof definition.hint === 'string' && definition.hint ? Array.from(definition.hint)[0] : symbol,
109
+ }
110
+ }
111
+
112
+ function defineSlot(slots, symbol, definition) {
113
+ slots[symbol] = normalizeSlot(symbol, definition)
114
+ }
115
+
116
+ /**
117
+ * Converte um padrão string em tokens — resultado cacheado por padrão.
118
+ *
119
+ * Token sem modificador:
120
+ * '#' → { type:'input', base:'#', test: /\d/.test }
121
+ *
122
+ * Token {expr}:
123
+ * '{0-4}' → { type:'input', base:'{expr}', test: ch>=0&&ch<=4, constraint:'0-4' }
124
+ * '{[^0]}'→ { type:'input', base:'{expr}', test: /[^0]/.test, constraint:'[^0]' }
125
+ */
126
+ function parse(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
127
+ const cacheKey = `${cacheId}:${slotsVersion}:${pattern}`
128
+ if (_parseCache.has(cacheKey)) return _parseCache.get(cacheKey)
129
+
130
+ const tokens = []
131
+ let i = 0
132
+
133
+ while (i < pattern.length) {
134
+ const ch = pattern[i]
135
+
136
+ // ── literal fixo [texto] ──────────────────────────────────────────────
137
+ if (ch === '[') {
138
+ const close = pattern.indexOf(']', i)
139
+ if (close === -1) throw new Error(`mask: colchete não fechado em "${pattern}"`)
140
+ tokens.push({ type: 'literal', value: pattern.slice(i + 1, close) })
141
+ i = close + 1
142
+ continue
143
+ }
144
+
145
+ // ── slot livre {expr} ─────────────────────────────────────────────────
146
+ if (ch === '{') {
147
+ const close = pattern.indexOf('}', i)
148
+ if (close === -1) throw new Error(`mask: chave não fechada em "${pattern}"`)
149
+ const expr = pattern.slice(i + 1, close)
150
+ if (!expr) throw new Error(`mask: expressão vazia em "${pattern}"`)
151
+ const { test, constraint } = resolveExpr(expr)
152
+ tokens.push({ type: 'input', base: '{expr}', test, constraint })
153
+ i = close + 1
154
+ continue
155
+ }
156
+
157
+ // ── tokens de input configuráveis ─────────────────────────────────────
158
+ if (slots[ch]) {
159
+ tokens.push({ type: 'input', base: ch, ...slots[ch] })
160
+ i++
161
+ continue
162
+ }
163
+
164
+ // ── literal implícito ─────────────────────────────────────────────────
165
+ tokens.push({ type: 'literal', value: ch })
166
+ i++
167
+ }
168
+
169
+ // congela os tokens para evitar mutação acidental no cache
170
+ Object.freeze(tokens)
171
+ _parseCache.set(cacheKey, tokens)
172
+ return tokens
173
+ }
174
+
175
+ /** Quantos slots de input um padrão tem */
176
+ function inputCount(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
177
+ return parse(pattern, slots, cacheId, slotsVersion).filter(t => t.type === 'input').length
178
+ }
179
+
180
+ // ─── Engine de aplicação ───────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Aplica tokens a chars de input já filtrados e validados.
184
+ * Retorna o valor mascarado, sem literais pendurados no final.
185
+ */
186
+ function applyTokens(tokens, inputChars) {
187
+ let masked = ''
188
+ let ci = 0
189
+
190
+ for (const token of tokens) {
191
+ if (ci >= inputChars.length) break
192
+
193
+ if (token.type === 'literal') {
194
+ masked += token.value
195
+ continue
196
+ }
197
+
198
+ if (token.test(inputChars[ci])) {
199
+ masked += inputChars[ci]
200
+ ci++
201
+ } else {
202
+ break
203
+ }
204
+ }
205
+
206
+ // remove literais pendurados no final sem input depois deles
207
+ let trimmed = masked
208
+ for (let i = tokens.length - 1; i >= 0; i--) {
209
+ const t = tokens[i]
210
+ if (t.type === 'literal') {
211
+ if (trimmed.endsWith(t.value)) trimmed = trimmed.slice(0, -t.value.length)
212
+ } else {
213
+ break
214
+ }
215
+ }
216
+
217
+ return trimmed
218
+ }
219
+
220
+ // ─── Seleção de padrão dinâmico ────────────────────────────────────────────
221
+
222
+ /**
223
+ * Dado um array de padrões e os chars de input,
224
+ * escolhe o menor padrão que ainda comporta todos os chars.
225
+ * Se nenhum comporta, usa o maior.
226
+ */
227
+ function selectPattern(patterns, chars, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
228
+ const sorted = [...patterns].sort((a, b) => inputCount(a, slots, cacheId, slotsVersion) - inputCount(b, slots, cacheId, slotsVersion))
229
+ for (const p of sorted) {
230
+ if (inputCount(p, slots, cacheId, slotsVersion) >= chars.length) return p
231
+ }
232
+ return sorted[sorted.length - 1]
233
+ }
234
+
235
+ // ─── Extração e validação de chars de input ────────────────────────────────
236
+
237
+ /**
238
+ * Extrai os chars de input de um valor (bruto ou mascarado),
239
+ * filtra literais, valida cada char contra o predicado do slot correspondente,
240
+ * e trunca ao limite do padrão escolhido.
241
+ *
242
+ * FIX: chars inválidos (ex: letras em campo #) são descartados antes de
243
+ * chegar ao applyTokens — garante raw correto mesmo em paste.
244
+ */
245
+ function defaultValidate() {
246
+ return true
247
+ }
248
+
249
+ function fullLength(pattern, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
250
+ return parse(pattern, slots, cacheId, slotsVersion).reduce((total, token) => {
251
+ if (token.type === 'literal') return total + token.value.length
252
+ return total + 1
253
+ }, 0)
254
+ }
255
+
256
+ function extractInputChars(value, patterns, validate = defaultValidate, slots = globalSlots, cacheId = 'global', slotsVersion = globalSlotsVersion) {
257
+ const patList = Array.isArray(patterns) ? patterns : [patterns]
258
+
259
+ // 1. coleta todos os literais para remover do valor
260
+ const allLiterals = new Set()
261
+ for (const p of patList) {
262
+ for (const t of parse(p, slots, cacheId, slotsVersion)) {
263
+ if (t.type === 'literal') {
264
+ for (const ch of t.value) allLiterals.add(ch)
265
+ }
266
+ }
267
+ }
268
+
269
+ // 2. remove literais — chars candidatos a input
270
+ const candidates = Array.from(String(value)).filter(ch => !allLiterals.has(ch))
271
+
272
+ // 3. escolhe o padrão para estes candidatos (antes de validar)
273
+ const chosen = selectPattern(patList, candidates, slots, cacheId, slotsVersion)
274
+ const inputTokens = parse(chosen, slots, cacheId, slotsVersion).filter(t => t.type === 'input')
275
+
276
+ // 4. valida cada char contra o predicado do slot correspondente
277
+ // chars que não passam no teste são descartados (não apenas bloqueados)
278
+ // isso garante que paste com chars inválidos produza raw correto
279
+ const valid = []
280
+ let ti = 0
281
+ for (const ch of candidates) {
282
+ if (ti >= inputTokens.length) break
283
+ if (inputTokens[ti].test(ch)) {
284
+ const next = [...valid, ch]
285
+ const nextRaw = next.join('')
286
+ const nextMasked = applyTokens(parse(chosen, slots, cacheId, slotsVersion), next)
287
+ const complete = next.length >= inputCount(chosen, slots, cacheId, slotsVersion)
288
+ if (!validate(nextRaw, nextMasked, complete)) break
289
+ valid.push(ch)
290
+ ti++
291
+ }
292
+ // char inválido para este slot → descartado silenciosamente
293
+ }
294
+
295
+ // 5. trunca ao limite do padrão (fonte da verdade do tamanho máximo)
296
+ return valid.slice(0, inputCount(chosen, slots, cacheId, slotsVersion))
297
+ }
298
+
299
+ // ─── Registro de máscaras nomeadas ────────────────────────────────────────
300
+
301
+ const registry = new Map()
302
+
303
+ // ─── API pública ───────────────────────────────────────────────────────────
304
+
305
+ /**
306
+ * mask(pattern, value) — aplica máscara, retorna string formatada (display)
307
+ *
308
+ * @param {string | string[]} pattern padrão único, array dinâmico, ou nome registrado
309
+ * @param {string} value valor bruto ou já mascarado
310
+ * @returns {string}
311
+ *
312
+ * @example
313
+ * mask('##[/]##[/]####', '01012025') // → '01/01/2025'
314
+ * mask(['[(]##[)] ####[-]####',
315
+ * '[(]##[)] #####[-]####'], '11987654321') // → '(11) 98765-4321'
316
+ * mask('date', '01012025') // → '01/01/2025' (nome registrado)
317
+ */
318
+ function mask(pattern, value) {
319
+ if (value == null) return ''
320
+ let validate
321
+ if (typeof pattern === 'string' && registry.has(pattern)) {
322
+ const entry = registry.get(pattern)
323
+ validate = entry.validate
324
+ pattern = entry.pattern
325
+ }
326
+ const str = String(value)
327
+ const patterns = Array.isArray(pattern) ? pattern : [pattern]
328
+ const chars = extractInputChars(str, patterns, validate, globalSlots, 'global', globalSlotsVersion)
329
+ const chosen = selectPattern(patterns, chars, globalSlots, 'global', globalSlotsVersion)
330
+ return applyTokens(parse(chosen, globalSlots, 'global', globalSlotsVersion), chars)
331
+ }
332
+
333
+ /**
334
+ * mask.raw(pattern, value) — devolve o raw passado pelo transform
335
+ *
336
+ * O raw É o transform. Não são dois valores — são um só.
337
+ *
338
+ * Sem transform: devolve a string crua (chars sem literais), sempre.
339
+ * Com transform: devolve exatamente o que transform retornar —
340
+ * parcial ou completo. O transform recebe (raw, masked, complete).
341
+ *
342
+ * @example
343
+ * mask.raw('##[/]##[/]####', '01/01/2025') // → '01012025'
344
+ * mask.raw('date', '01/01/2025') // → Date(2025-01-01) (com transform)
345
+ * mask.raw('date', '01/01') // → null (transform decidiu)
346
+ */
347
+ mask.raw = function (pattern, value) {
348
+ if (value == null) return ''
349
+
350
+ let transform
351
+ let validate
352
+ if (typeof pattern === 'string' && registry.has(pattern)) {
353
+ const entry = registry.get(pattern)
354
+ transform = entry.transform
355
+ validate = entry.validate
356
+ pattern = entry.pattern
357
+ }
358
+
359
+ const patterns = Array.isArray(pattern) ? pattern : [pattern]
360
+ const raw = extractInputChars(String(value), patterns, validate, globalSlots, 'global', globalSlotsVersion).join('')
361
+
362
+ if (!transform) return raw
363
+
364
+ const complete = patterns.some(p => raw.length >= inputCount(p, globalSlots, 'global', globalSlotsVersion))
365
+ return transform(raw, mask(pattern, value), complete)
366
+ }
367
+
368
+ /**
369
+ * mask.is(pattern, value) — verifica se o valor preenche o padrão completamente
370
+ *
371
+ * @example
372
+ * mask.is('##[/]##[/]####', '01/01/2025') // → true
373
+ * mask.is('##[/]##[/]####', '01/01') // → false
374
+ */
375
+ mask.is = function (pattern, value) {
376
+ if (value == null) return false
377
+ let validate
378
+ if (typeof pattern === 'string' && registry.has(pattern)) {
379
+ const entry = registry.get(pattern)
380
+ validate = entry.validate
381
+ pattern = entry.pattern
382
+ }
383
+ const patterns = Array.isArray(pattern) ? pattern : [pattern]
384
+ const chars = extractInputChars(String(value), patterns, validate, globalSlots, 'global', globalSlotsVersion)
385
+ return patterns.some(p => chars.length >= inputCount(p, globalSlots, 'global', globalSlotsVersion))
386
+ }
387
+
388
+ /**
389
+ * mask.hint(pattern) — placeholder legível para o campo
390
+ *
391
+ * @example
392
+ * mask.hint('##[/]##[/]####') // → '00/00/0000'
393
+ * mask.hint('[(]##[)] #####[-]####') // → '(00) 00000-0000'
394
+ * mask.hint('{[0-4]}###') // → '0###' (primeiro char do range)
395
+ */
396
+ mask.hint = function (pattern) {
397
+ if (typeof pattern === 'string' && registry.has(pattern)) {
398
+ pattern = registry.get(pattern).pattern
399
+ }
400
+ const p = Array.isArray(pattern) ? pattern[pattern.length - 1] : pattern
401
+ return parse(p, globalSlots, 'global', globalSlotsVersion)
402
+ .map(t => {
403
+ if (t.type === 'literal') return t.value
404
+ if (t.constraint) {
405
+ if (t.constraint.length === 3 && t.constraint[1] === '-') return t.constraint[0]
406
+ if (/[\\[^(|]/.test(t.constraint) || t.constraint.startsWith('\\')) return '_'
407
+ return t.constraint[0]
408
+ }
409
+ return t.hint || '_'
410
+ })
411
+ .join('')
412
+ }
413
+
414
+ /**
415
+ * mask.rawLength(pattern, value) — comprimento do raw atual (chars de input preenchidos)
416
+ *
417
+ * Equivale a mask.raw(pattern, value).length mas sem passar pelo transform —
418
+ * sempre retorna um número, independente do transform definido.
419
+ *
420
+ * Útil para: progress bars, validação incremental, contadores de caracteres.
421
+ *
422
+ * @example
423
+ * mask.rawLength('##[/]##[/]####', '01/01') // → 4 (digitando)
424
+ * mask.rawLength('##[/]##[/]####', '01/01/2025') // → 8 (completo)
425
+ * mask.rawLength('date', '01/01/2025') // → 8 (funciona com nome registrado)
426
+ *
427
+ * // Progress bar:
428
+ * const pct = mask.rawLength('cpf', value) / mask.patternLength('cpf') * 100
429
+ */
430
+ mask.rawLength = function (pattern, value) {
431
+ if (value == null) return 0
432
+ let p = pattern
433
+ let validate
434
+ if (typeof p === 'string' && registry.has(p)) {
435
+ const entry = registry.get(p)
436
+ validate = entry.validate
437
+ p = entry.pattern
438
+ }
439
+ const patterns = Array.isArray(p) ? p : [p]
440
+ // Aplica a máscara e conta os chars do resultado — fonte da verdade
441
+ // é o valor mascarado, não o valor bruto (que pode ter chars não validados)
442
+ const masked = mask(p, String(value))
443
+ return extractInputChars(masked, patterns, validate, globalSlots, 'global', globalSlotsVersion).length
444
+ }
445
+
446
+ /**
447
+ * mask.patternLength(pattern) — comprimento total do valor mascarado completo
448
+ *
449
+ * Conta slots de input como 1 caractere e literais pelo seu tamanho real.
450
+ * Para padrões dinâmicos (array), retorna o maior comprimento mascarado.
451
+ *
452
+ * Útil para: limites de caracteres, progress bars, pré-alocação de buffers.
453
+ *
454
+ * @example
455
+ * mask.patternLength('##[/]##[/]####') // → 10
456
+ * mask.patternLength('###[.]###[.]###[-]##') // → 14 (CPF)
457
+ * mask.patternLength(['[(]##[)] ####[-]####',
458
+ * '[(]##[)] #####[-]####']) // → 15 (maior padrão)
459
+ * mask.patternLength('date') // → 10 (nome registrado)
460
+ */
461
+ mask.patternLength = function (pattern) {
462
+ if (typeof pattern === 'string' && registry.has(pattern)) {
463
+ pattern = registry.get(pattern).pattern
464
+ }
465
+ const patterns = Array.isArray(pattern) ? pattern : [pattern]
466
+ return Math.max(...patterns.map(p => fullLength(p, globalSlots, 'global', globalSlotsVersion)))
467
+ }
468
+
469
+ /**
470
+ * mask.format(pattern, rawValue) — formata um valor vindo da API (sem máscara)
471
+ *
472
+ * Alias semântico de mask() — deixa clara a intenção de "formatar para exibição"
473
+ * versus "aplicar máscara enquanto o usuário digita".
474
+ *
475
+ * @example
476
+ * mask.format('cpf', user.cpf) // → '123.456.789-09'
477
+ * mask.format('phone', contact.phone) // → '(11) 98765-4321'
478
+ * mask.format('date', '01012025') // → '01/01/2025'
479
+ */
480
+ mask.format = function (pattern, value) {
481
+ return mask(pattern, value)
482
+ }
483
+
484
+ /**
485
+ * mask.define(name, definition) — registra uma máscara nomeada
486
+ *
487
+ * @param {string} name
488
+ * @param {{ pattern: string | string[], transform?: (raw, masked, complete) => any, validate?: (raw, masked, complete) => boolean }} definition
489
+ *
490
+ * @example
491
+ * mask.define('date', {
492
+ * pattern: '##[/]##[/]####',
493
+ * transform: (raw, masked, complete) => {
494
+ * if (!complete) return null
495
+ * const dt = new Date(`${raw.slice(4,8)}-${raw.slice(2,4)}-${raw.slice(0,2)}`)
496
+ * return isNaN(dt) ? null : dt
497
+ * },
498
+ * })
499
+ *
500
+ * mask.define('money', {
501
+ * pattern: '########[,]##',
502
+ * transform: raw => parseInt(raw || '0', 10) / 100,
503
+ * })
504
+ *
505
+ * mask.define('month', {
506
+ * pattern: '{0-1}#',
507
+ * validate: (raw, masked, complete) => !complete || Number(raw) >= 1 && Number(raw) <= 12,
508
+ * })
509
+ */
510
+ mask.define = function (name, definition) {
511
+ if (!definition?.pattern) throw new Error(`mask.define: "${name}" precisa de um pattern`)
512
+ if (definition.validate && typeof definition.validate !== 'function') {
513
+ throw new Error(`mask.define: "${name}" validate precisa ser uma função`)
514
+ }
515
+ registry.set(name, definition)
516
+ }
517
+
518
+ /**
519
+ * mask.undefine(name) — remove uma máscara do registro
520
+ *
521
+ * @example
522
+ * mask.undefine('date')
523
+ * mask.is('date', '01/01/2025') // throws — 'date' não existe mais
524
+ */
525
+ mask.undefine = function (name) {
526
+ registry.delete(name)
527
+ }
528
+
529
+ /**
530
+ * mask.names() — lista todas as máscaras registradas
531
+ *
532
+ * @example
533
+ * mask.names() // → ['cpf', 'phone', 'date', 'money', ...]
534
+ */
535
+ mask.names = function () {
536
+ return Array.from(registry.keys())
537
+ }
538
+
539
+ /**
540
+ * mask.defineSlot(symbol, definition) — cria ou sobrescreve um token de input.
541
+ *
542
+ * @example
543
+ * mask.defineSlot('N', { test: ch => /\d/.test(ch), hint: '0' })
544
+ * mask('NNN[-]NN', '12345') // → '123-45'
545
+ */
546
+ mask.defineSlot = function (symbol, definition) {
547
+ defineSlot(globalSlots, symbol, definition)
548
+ globalSlotsVersion++
549
+ }
550
+
551
+ /**
552
+ * mask.undefineSlot(symbol) — remove um token customizado.
553
+ * Tokens padrão (#, @, *) voltam ao comportamento original.
554
+ */
555
+ mask.undefineSlot = function (symbol) {
556
+ if (DEFAULT_SLOTS[symbol]) globalSlots[symbol] = DEFAULT_SLOTS[symbol]
557
+ else delete globalSlots[symbol]
558
+ globalSlotsVersion++
559
+ }
560
+
561
+ /**
562
+ * mask.slots() — lista os símbolos disponíveis na linguagem atual.
563
+ */
564
+ mask.slots = function () {
565
+ return Object.keys(globalSlots)
566
+ }
567
+
568
+ /**
569
+ * mask.on(input, pattern, options?) — vincula máscara a um input DOM
570
+ *
571
+ * Framework-agnostic. Retorna função de cleanup.
572
+ *
573
+ * options:
574
+ * onValue(raw) — valor limpo / transformado a cada mudança
575
+ * onMasked(v) — valor mascarado a cada mudança
576
+ *
577
+ * @example
578
+ * // Vanilla
579
+ * const off = mask.on(el, 'cpf', { onValue: v => setState(v) })
580
+ * off() // remove listeners
581
+ *
582
+ * // React
583
+ * useEffect(() => mask.on(ref.current, 'date', {
584
+ * onValue: date => setValue(date), // recebe Date | null
585
+ * }), [])
586
+ *
587
+ * // Vue
588
+ * onMounted(() => mask.on(inputRef.value, 'phone', {
589
+ * onValue: v => emit('update:modelValue', v),
590
+ * }))
591
+ */
592
+ mask.on = function (input, pattern, options = {}) {
593
+ const { onValue, onMasked } = options
594
+
595
+ function resolvePatterns() {
596
+ let p = pattern
597
+ if (typeof p === 'string' && registry.has(p)) p = registry.get(p).pattern
598
+ return Array.isArray(p) ? p : [p]
599
+ }
600
+
601
+ // Bloqueia keydown quando já no limite máximo —
602
+ // evita o flash de um frame com char excedente antes do handler corrigir
603
+ function onKeydown(e) {
604
+ if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return
605
+ const patterns = resolvePatterns()
606
+ let validate
607
+ if (typeof pattern === 'string' && registry.has(pattern)) validate = registry.get(pattern).validate
608
+ const chars = extractInputChars(e.target.value, patterns, validate, globalSlots, 'global', globalSlotsVersion)
609
+ const maxLimit = Math.max(...patterns.map(p => inputCount(p, globalSlots, 'global', globalSlotsVersion)))
610
+ if (chars.length >= maxLimit) e.preventDefault()
611
+ }
612
+
613
+ // Handler de input: aplica máscara, preserva cursor, dispara callbacks
614
+ function handler(e) {
615
+ const el = e.target
616
+ const raw = el.value
617
+ const masked = mask(pattern, raw)
618
+ const cursor = el.selectionStart ?? masked.length
619
+ const diff = masked.length - raw.length
620
+
621
+ el.value = masked
622
+
623
+ requestAnimationFrame(() => {
624
+ const pos = Math.max(0, cursor + diff)
625
+ el.setSelectionRange(pos, pos)
626
+ })
627
+
628
+ onMasked?.(masked)
629
+ onValue?.(mask.raw(pattern, masked))
630
+ }
631
+
632
+ input.addEventListener('keydown', onKeydown)
633
+ input.addEventListener('input', handler)
634
+
635
+ return () => {
636
+ input.removeEventListener('keydown', onKeydown)
637
+ input.removeEventListener('input', handler)
638
+ }
639
+ }
640
+
641
+ // ─── Instância isolada ────────────────────────────────────────────────────
642
+
643
+ /**
644
+ * Cria uma instância independente do engine com registry próprio
645
+ * e presets opcionais — sem compartilhar estado com a instância global.
646
+ *
647
+ * Útil para:
648
+ * - Múltiplos contextos num mesmo projeto (BR + US, admin + cliente)
649
+ * - Bibliotecas que não querem poluir o registry global
650
+ * - Testes com estado isolado
651
+ * - Presets reutilizáveis distribuídos como pacote
652
+ *
653
+ * @param {Record<string, MaskDefinition>} presets máscaras pré-configuradas
654
+ * @returns instância com a mesma API de mask, mas registry isolado
655
+ *
656
+ * @example
657
+ * // Instância com presets brasileiros
658
+ * export const maskBR = mask.create({
659
+ * cpf: { pattern: '###[.]###[.]###[-]##' },
660
+ * cnpj: { pattern: '##[.]###[.]###[/]####[-]##' },
661
+ * phone: { pattern: ['[(]##[)] ####[-]####', '[(]##[)] #####[-]####'] },
662
+ * cep: { pattern: '#####[-]###', transform: (r,m,c) => c ? r : null },
663
+ * date: {
664
+ * pattern: '##[/]##[/]####',
665
+ * transform: (raw, masked, complete) => {
666
+ * if (!complete) return null
667
+ * const dt = new Date(`${raw.slice(4,8)}-${raw.slice(2,4)}-${raw.slice(0,2)}T12:00:00`)
668
+ * return isNaN(dt) ? null : dt
669
+ * },
670
+ * },
671
+ * money: {
672
+ * pattern: '########[,]##',
673
+ * transform: raw => parseInt(raw || '0', 10) / 100,
674
+ * },
675
+ * })
676
+ *
677
+ * // Instância com presets americanos
678
+ * export const maskUS = mask.create({
679
+ * ssn: { pattern: '###[-]##[-]####' },
680
+ * zip: { pattern: '#####[-]####' },
681
+ * phone: { pattern: '({0-9}{0-9}{0-9}) ###[-]####' },
682
+ * date: { pattern: '##{/}##[/]####' },
683
+ * })
684
+ *
685
+ * // Uso — mesma API, registry isolado
686
+ * maskBR('cpf', '12345678909') // → '123.456.789-09'
687
+ * maskBR.raw('date', '01/01/2025') // → Date(2025-01-01)
688
+ * maskBR.is('phone', '(11) 98765-4321')// → true
689
+ * maskBR.on(el, 'cep', { onValue: v => setCep(v) })
690
+ *
691
+ * // Registros da instância não afetam a global nem outras instâncias
692
+ * maskBR.define('rg', { pattern: '##[.]###[.]###[-]#' })
693
+ * maskUS.names() // → ['ssn', 'zip', 'phone', 'date'] (sem 'rg')
694
+ * mask.names() // → [] (global intocada)
695
+ */
696
+ mask.create = function (presets = {}) {
697
+ // registry isolado para esta instância
698
+ const localRegistry = new Map()
699
+ const localSlots = { ...globalSlots }
700
+ const localCacheId = `local-${++slotLanguageSeq}`
701
+ let localSlotsVersion = globalSlotsVersion
702
+
703
+ // registra presets iniciais
704
+ for (const [name, def] of Object.entries(presets)) {
705
+ if (!def?.pattern) throw new Error(`mask.create: "${name}" precisa de um pattern`)
706
+ localRegistry.set(name, def)
707
+ }
708
+
709
+ // resolve pattern de um nome neste registry local
710
+ function resolveLocal(pattern) {
711
+ if (typeof pattern === 'string' && localRegistry.has(pattern)) {
712
+ return localRegistry.get(pattern).pattern
713
+ }
714
+ return pattern
715
+ }
716
+
717
+ function resolveLocalEntry(pattern) {
718
+ if (typeof pattern === 'string' && localRegistry.has(pattern)) {
719
+ return localRegistry.get(pattern)
720
+ }
721
+ return undefined
722
+ }
723
+
724
+ // ── API da instância — espelha mask.* usando o registry local ─────────
725
+
726
+ function instance(pattern, value) {
727
+ if (value == null) return ''
728
+ const p = resolveLocal(pattern)
729
+ const pats = Array.isArray(p) ? p : [p]
730
+ const chars = extractInputChars(String(value), pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
731
+ const chosen = selectPattern(pats, chars, localSlots, localCacheId, localSlotsVersion)
732
+ return applyTokens(parse(chosen, localSlots, localCacheId, localSlotsVersion), chars)
733
+ }
734
+
735
+ instance.raw = function (pattern, value) {
736
+ if (value == null) return ''
737
+ let transform
738
+ let validate
739
+ if (typeof pattern === 'string' && localRegistry.has(pattern)) {
740
+ const entry = localRegistry.get(pattern)
741
+ transform = entry.transform
742
+ validate = entry.validate
743
+ pattern = entry.pattern
744
+ }
745
+ const pats = Array.isArray(pattern) ? pattern : [pattern]
746
+ const raw = extractInputChars(String(value), pats, validate, localSlots, localCacheId, localSlotsVersion).join('')
747
+ if (!transform) return raw
748
+ const complete = pats.some(p => raw.length >= inputCount(p, localSlots, localCacheId, localSlotsVersion))
749
+ return transform(raw, instance(pattern, value), complete)
750
+ }
751
+
752
+ instance.is = function (pattern, value) {
753
+ if (value == null) return false
754
+ const p = resolveLocal(pattern)
755
+ const pats = Array.isArray(p) ? p : [p]
756
+ const chars = extractInputChars(String(value), pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
757
+ return pats.some(pt => chars.length >= inputCount(pt, localSlots, localCacheId, localSlotsVersion))
758
+ }
759
+
760
+ instance.hint = function (pattern) {
761
+ const p = resolveLocal(pattern)
762
+ const pat = Array.isArray(p) ? p[p.length - 1] : p
763
+ return parse(pat, localSlots, localCacheId, localSlotsVersion).map(t => {
764
+ if (t.type === 'literal') return t.value
765
+ if (t.constraint) {
766
+ if (t.constraint.length === 3 && t.constraint[1] === '-') return t.constraint[0]
767
+ if (/[\\[^(|]/.test(t.constraint) || t.constraint.startsWith('\\')) return '_'
768
+ return t.constraint[0]
769
+ }
770
+ return t.hint || '_'
771
+ }).join('')
772
+ }
773
+
774
+ instance.format = function (pattern, value) {
775
+ return instance(pattern, value)
776
+ }
777
+
778
+ instance.define = function (name, definition) {
779
+ if (!definition?.pattern) throw new Error(`mask.define: "${name}" precisa de um pattern`)
780
+ if (definition.validate && typeof definition.validate !== 'function') {
781
+ throw new Error(`mask.define: "${name}" validate precisa ser uma função`)
782
+ }
783
+ localRegistry.set(name, definition)
784
+ }
785
+
786
+ instance.undefine = function (name) {
787
+ localRegistry.delete(name)
788
+ }
789
+
790
+ instance.names = function () {
791
+ return Array.from(localRegistry.keys())
792
+ }
793
+
794
+ instance.rawLength = function (pattern, value) {
795
+ if (value == null) return 0
796
+ const p = resolveLocal(pattern)
797
+ const pats = Array.isArray(p) ? p : [p]
798
+ const masked = instance(pattern, String(value))
799
+ return extractInputChars(masked, pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion).length
800
+ }
801
+
802
+ instance.patternLength = function (pattern) {
803
+ const p = resolveLocal(pattern)
804
+ const pats = Array.isArray(p) ? p : [p]
805
+ return Math.max(...pats.map(p => fullLength(p, localSlots, localCacheId, localSlotsVersion)))
806
+ }
807
+
808
+ instance.on = function (input, pattern, options = {}) {
809
+ const { onValue, onMasked } = options
810
+
811
+ function resolvePatterns() {
812
+ const p = resolveLocal(pattern)
813
+ return Array.isArray(p) ? p : [p]
814
+ }
815
+
816
+ function onKeydown(e) {
817
+ if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return
818
+ const pats = resolvePatterns()
819
+ const chars = extractInputChars(e.target.value, pats, resolveLocalEntry(pattern)?.validate, localSlots, localCacheId, localSlotsVersion)
820
+ const maxLimit = Math.max(...pats.map(p => inputCount(p, localSlots, localCacheId, localSlotsVersion)))
821
+ if (chars.length >= maxLimit) e.preventDefault()
822
+ }
823
+
824
+ function handler(e) {
825
+ const el = e.target
826
+ const raw = el.value
827
+ const masked = instance(pattern, raw)
828
+ const cursor = el.selectionStart ?? masked.length
829
+ const diff = masked.length - raw.length
830
+ el.value = masked
831
+ requestAnimationFrame(() => {
832
+ const pos = Math.max(0, cursor + diff)
833
+ el.setSelectionRange(pos, pos)
834
+ })
835
+ onMasked?.(masked)
836
+ onValue?.(instance.raw(pattern, masked))
837
+ }
838
+
839
+ input.addEventListener('keydown', onKeydown)
840
+ input.addEventListener('input', handler)
841
+
842
+ return () => {
843
+ input.removeEventListener('keydown', onKeydown)
844
+ input.removeEventListener('input', handler)
845
+ }
846
+ }
847
+
848
+ instance.defineSlot = function (symbol, definition) {
849
+ defineSlot(localSlots, symbol, definition)
850
+ localSlotsVersion++
851
+ }
852
+
853
+ instance.undefineSlot = function (symbol) {
854
+ if (DEFAULT_SLOTS[symbol]) localSlots[symbol] = DEFAULT_SLOTS[symbol]
855
+ else delete localSlots[symbol]
856
+ localSlotsVersion++
857
+ }
858
+
859
+ instance.slots = function () {
860
+ return Object.keys(localSlots)
861
+ }
862
+
863
+ // permite encadear mais defines após create()
864
+ instance.create = mask.create
865
+
866
+ return instance
867
+ }
868
+
869
+ // ─── Export ────────────────────────────────────────────────────────────────
870
+
871
+ // export { mask }
872
+ // export default mask
873
+
874
+ module.exports = mask