Skip to content

Commit 0fede1c

Browse files
committed
refactor(mask): Cleaned up mask parser implementation
1 parent d7e5e98 commit 0fede1c

File tree

1 file changed

+37
-17
lines changed

1 file changed

+37
-17
lines changed

src/components/mask-input/mask-parser.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ type MaskReplaceResult = {
3636
const MASK_FLAGS = new Set('aACL09#&?');
3737
const MASK_REQUIRED_FLAGS = new Set('0#LA&');
3838

39+
const ESCAPE_CHAR = '\\';
40+
const DEFAULT_FORMAT = 'CCCCCCCCCC';
41+
const DEFAULT_PROMPT = '_';
42+
3943
const ASCII_ZERO = 0x0030;
4044
const DIGIT_ZERO_CODEPOINTS = [
4145
ASCII_ZERO, // ASCII
@@ -97,8 +101,8 @@ function validate(char: string, flag: string): boolean {
97101

98102
/** Default mask parser options */
99103
const MaskDefaultOptions: MaskOptionsInternal = {
100-
format: 'CCCCCCCCCC',
101-
promptCharacter: '_',
104+
format: DEFAULT_FORMAT,
105+
promptCharacter: DEFAULT_PROMPT,
102106
};
103107

104108
/**
@@ -124,7 +128,7 @@ export class MaskParser {
124128
* Returns a set of the all the literal positions in the mask.
125129
* These positions are fixed characters that are not part of the input.
126130
*/
127-
public get literalPositions(): Set<number> {
131+
public get literalPositions(): ReadonlySet<number> {
128132
return this._literalPositions;
129133
}
130134

@@ -187,6 +191,10 @@ export class MaskParser {
187191
this._parseMaskLiterals();
188192
}
189193

194+
private _isEscapedFlag(char: string, nextChar: string): boolean {
195+
return char === ESCAPE_CHAR && MASK_FLAGS.has(nextChar);
196+
}
197+
190198
/**
191199
* Parses the mask format string to identify literal characters and
192200
* create the escaped mask. This method is called whenever the mask format changes.
@@ -203,7 +211,7 @@ export class MaskParser {
203211
for (let i = 0; i < length; i++) {
204212
const [current, next] = [mask.charAt(i), mask.charAt(i + 1)];
205213

206-
if (current === '\\' && MASK_FLAGS.has(next)) {
214+
if (this._isEscapedFlag(current, next)) {
207215
// Escaped character - push next as a literal character and skip processing it
208216
this._literals.set(currentPos, next);
209217
escapedMaskChars.push(next);
@@ -289,6 +297,14 @@ export class MaskParser {
289297
/**
290298
* Replaces a segment of the masked string with new input, simulating typing or pasting.
291299
* It handles clearing the selected range and inserting new characters according to the mask.
300+
*
301+
* @example
302+
* ```ts
303+
* const parser = new MaskParser({ format: '00/00/0000' });
304+
* const current = '__/__/____';
305+
* const result = parser.replace(current, '12', 0, 0);
306+
* // result.value = '12/__/____', result.end = 2
307+
* ```
292308
*/
293309
public replace(
294310
maskString: string,
@@ -298,12 +314,12 @@ export class MaskParser {
298314
): MaskReplaceResult {
299315
const literalPositions = this.literalPositions;
300316
const escapedMask = this._escapedMask;
301-
const length = this._escapedMask.length;
317+
const length = escapedMask.length;
302318
const prompt = this.prompt;
303319
const endBoundary = Math.min(end, length);
304320

305321
// Initialize the array for the masked string or get a fresh mask with prompts and/or literals
306-
const maskedChars = Array.from(maskString || this.apply(''));
322+
const maskedChars = maskString ? [...maskString] : [...this.apply('')];
307323

308324
const inputChars = Array.from(replaceUnicodeNumbers(value));
309325
const inputLength = inputChars.length;
@@ -358,17 +374,16 @@ export class MaskParser {
358374
const literalPositions = this.literalPositions;
359375
const prompt = this.prompt;
360376
const length = masked.length;
361-
362-
let result = '';
377+
const result: string[] = [];
363378

364379
for (let i = 0; i < length; i++) {
365380
const char = masked[i];
366381
if (!literalPositions.has(i) && char !== prompt) {
367-
result += char;
382+
result.push(char);
368383
}
369384
}
370385

371-
return result;
386+
return result.join('');
372387
}
373388

374389
/**
@@ -379,17 +394,21 @@ export class MaskParser {
379394
const prompt = this.prompt;
380395

381396
return this._requiredPositions.every((position) => {
382-
const char = input.charAt(position);
383-
return (
384-
validate(char, this._escapedMask.charAt(position)) && char !== prompt
385-
);
397+
const char = input[position];
398+
return validate(char, this._escapedMask[position]) && char !== prompt;
386399
});
387400
}
388401

389402
/**
390403
* Applies the mask format to an input string. This attempts to fit the input
391404
* into the mask from left to right, filling valid positions and skipping invalid
392405
* input characters.
406+
*
407+
* @example
408+
* ```ts
409+
* const parser = new MaskParser({ format: '00/00/0000' });
410+
* parser.apply('12252023'); // Returns '12/25/2023'
411+
* ```
393412
*/
394413
public apply(input = ''): string {
395414
const literals = this._literals;
@@ -398,7 +417,7 @@ export class MaskParser {
398417
const length = escapedMask.length;
399418

400419
// Initialize the result array with prompt characters
401-
const result = new Array(escapedMask.length).fill(prompt);
420+
const result = new Array(length).fill(prompt);
402421

403422
// Place all literal characters into the result array
404423
for (const [position, literal] of literals.entries()) {
@@ -424,10 +443,11 @@ export class MaskParser {
424443
continue;
425444
}
426445

427-
if (validate(normalizedInput.charAt(inputIndex), escapedMask.charAt(i))) {
428-
result[i] = normalizedInput.charAt(inputIndex);
446+
if (validate(normalizedInput[inputIndex], escapedMask[i])) {
447+
result[i] = normalizedInput[inputIndex];
429448
}
430449

450+
// Always advance - invalid characters are consumed/skipped
431451
inputIndex++;
432452
}
433453

0 commit comments

Comments
 (0)