diff --git a/src/web/AdvancedTextInputMaskListener.ts b/src/web/AdvancedTextInputMaskListener.ts index 5e2b646..cead090 100644 --- a/src/web/AdvancedTextInputMaskListener.ts +++ b/src/web/AdvancedTextInputMaskListener.ts @@ -27,8 +27,6 @@ class MaskedTextChangedListener { public textField: Field | null = null; public allowedKeys: string; - private afterText: string = ''; - constructor( primaryFormat: string, affineFormats: string[] = [], @@ -49,6 +47,41 @@ class MaskedTextChangedListener { this.allowedKeys = allowedKeys; } + private applyMaskAndSetText = ( + textField: Field, + rawText: string, + caretPosition: number, + isDeletion: boolean, + autocomplete?: boolean + ): MaskResult => { + const filteredText = this.allowedKeys + ? [...rawText].filter((char) => this.allowedKeys.includes(char)).join('') + : rawText; + + const caretGravity = { + type: isDeletion ? CaretGravityType.Backward : CaretGravityType.Forward, + autoskip: isDeletion ? this.autoskip : false, + autocomplete: isDeletion ? false : autocomplete ?? this.autocomplete, + }; + + const textAndCaret = new CaretString( + filteredText, + caretPosition, + caretGravity + ); + + const mask = this.pickMask(textAndCaret); + const result = mask.apply(textAndCaret); + + textField.value = result.formattedText.string; + textField.setSelectionRange( + result.formattedText.caretPosition, + result.formattedText.caretPosition + ); + + return result; + }; + public get primaryMask(): Mask { return this.maskGetOrCreate(this.primaryFormat, this.customNotations); } @@ -58,26 +91,15 @@ class MaskedTextChangedListener { return; } - const newText = this.allowedKeys - ? [...text].filter((char) => this.allowedKeys.includes(char)).join('') - : text; + const isDeletion = this.textField.value.length > text.length; - const useAutocomplete = autocomplete ?? this.autocomplete; - const textAndCaret = new CaretString(newText, newText.length, { - type: CaretGravityType.Forward, - autocomplete: useAutocomplete, - autoskip: false, - }); - - const result: MaskResult = this.pickMask(textAndCaret).apply(textAndCaret); - this.textField.value = result.formattedText.string; - - this.textField.setSelectionRange( - result.formattedText.caretPosition, - result.formattedText.caretPosition + this.applyMaskAndSetText( + this.textField, + text, + text.length, + isDeletion, + autocomplete ); - - this.afterText = result.formattedText.string; } public setAllowedKeys = (allowedKeys: string): void => { @@ -127,30 +149,8 @@ class MaskedTextChangedListener { const selectionStart = textField.selectionStart || 0; const isInside = selectionStart < text.length; const caretPosition = isDeletion || isInside ? selectionStart : text.length; - const useAutocomplete = isDeletion ? false : this.autocomplete; - const useAutoskip = isDeletion ? this.autoskip : false; - const caretGravity = { - type: isDeletion ? CaretGravityType.Backward : CaretGravityType.Forward, - autoskip: useAutoskip, - autocomplete: useAutocomplete, - }; - const newText = this.allowedKeys - ? [...text].filter((char) => this.allowedKeys.includes(char)).join('') - : text; - const textAndCaret = new CaretString(newText, caretPosition, caretGravity); - const mask = this.pickMask(textAndCaret); - const result = mask.apply(textAndCaret); - - textField.value = result.formattedText.string; - textField.setSelectionRange( - result.formattedText.caretPosition, - result.formattedText.caretPosition - ); - - this.afterText = result.formattedText.string; - - return result; + return this.applyMaskAndSetText(textField, text, caretPosition, isDeletion); }; handleFocus = ( @@ -159,61 +159,44 @@ class MaskedTextChangedListener { if (this.autocomplete) { const textField = event.target as unknown as HTMLInputElement; const text = textField.value.length > 0 ? textField.value : ''; - const textAndCaret = new CaretString(text, text.length, { - type: CaretGravityType.Forward, - autocomplete: this.autocomplete, - autoskip: false, - }); - const result = this.pickMask(textAndCaret).apply(textAndCaret); - - this.afterText = result.formattedText.string; - textField.value = this.afterText; - - textField.setSelectionRange( - result.formattedText.caretPosition, - result.formattedText.caretPosition - ); + + this.applyMaskAndSetText(textField, text, text.length, false, true); } }; pickMask = (text: CaretString): Mask => { - if (this.affineFormats.length === 0) { + if (!this.affineFormats.length) { return this.primaryMask; } - const primaryAffinity = this.calculateAffinity(this.primaryMask, text); - const masksAndAffinities: MaskAffinity[] = []; - - for (const format of this.affineFormats) { - const candidateMask = this.maskGetOrCreate(format, this.customNotations); - const affinity = this.calculateAffinity(candidateMask, text); - masksAndAffinities.push({ mask: candidateMask, affinity }); - } - - masksAndAffinities.sort((a, b) => b.affinity - a.affinity); - - let insertIndex = -1; - for (let i = 0; i < masksAndAffinities.length; i++) { - const affinity = masksAndAffinities[i]?.affinity; - if (affinity && primaryAffinity === affinity) { - insertIndex = i; - break; - } - } - - if (insertIndex >= 0) { - masksAndAffinities.splice(insertIndex, 0, { + const candidates: MaskAffinity[] = [ + { mask: this.primaryMask, - affinity: primaryAffinity, - }); - } else { - masksAndAffinities.push({ - mask: this.primaryMask, - affinity: primaryAffinity, - }); - } - - return masksAndAffinities[0]!.mask; + affinity: calculateAffinityOfMask( + this.affinityCalculationStrategy, + this.primaryMask, + text + ), + }, + ...this.affineFormats.map((format) => { + const candidateMask = this.maskGetOrCreate( + format, + this.customNotations + ); + return { + mask: candidateMask, + affinity: calculateAffinityOfMask( + this.affinityCalculationStrategy, + candidateMask, + text + ), + }; + }), + ]; + + candidates.sort((a, b) => b.affinity - a.affinity); + + return candidates[0]!.mask; }; private maskGetOrCreate = ( @@ -223,14 +206,6 @@ class MaskedTextChangedListener { this.rightToLeft ? RTLMask.getOrCreate(format, customNotations) : Mask.getOrCreate(format, customNotations); - - private calculateAffinity(mask: Mask, text: CaretString): number { - return calculateAffinityOfMask( - this.affinityCalculationStrategy, - mask, - text - ); - } } export default MaskedTextChangedListener; diff --git a/src/web/helper/AutoCompelitionStack.ts b/src/web/helper/AutoCompelitionStack.ts new file mode 100644 index 0000000..8e60530 --- /dev/null +++ b/src/web/helper/AutoCompelitionStack.ts @@ -0,0 +1,21 @@ +import type { Next } from '../model/types'; + +class AutocompletionStack extends Array { + push(item: Next | null): number { + if (item == null) { + this.length = 0; + return 0; + } + return super.push(item); + } + + pop(): Next { + return super.pop()!; + } + + empty(): boolean { + return this.length === 0; + } +} + +export default AutocompletionStack; diff --git a/src/web/helper/Compiler.ts b/src/web/helper/Compiler.ts index de71006..197434d 100644 --- a/src/web/helper/Compiler.ts +++ b/src/web/helper/Compiler.ts @@ -7,7 +7,22 @@ import ValueState from '../model/state/ValueState'; import type { StateType } from '../model/types'; import FormatSanitizer from './FormatSanitizer'; import type { Notation } from '../../types'; -import { FIXED_STATE_TYPES, OPTIONAL_STATE_TYPES } from '../model/constants'; +import { + CLOSE_CURLY_BRACKET, + CLOSE_SQUARE_BRACKET, + ELLIPSIS_CHARACTER, + ESCAPE_CHARACTER, + FIXED_ALPHA_NUMERIC_CHARACTER, + FIXED_LITERAL_CHARACTER, + FIXED_NUMERIC_CHARACTER, + FIXED_STATE_TYPES, + OPEN_CURLY_BRACKET, + OPEN_SQUARE_BRACKET, + OPTIONAL_ALPHA_NUMERIC_CHARACTER, + OPTIONAL_LITERAL_CHARACTER, + OPTIONAL_NUMERIC_CHARACTER, + OPTIONAL_STATE_TYPES, +} from '../model/constants'; import FormatError from './FormatError'; export default class Compiler { @@ -35,19 +50,19 @@ export default class Compiler { const char = formatString.charAt(0); switch (char) { - case '[': - if (lastCharacter !== '\\') { + case OPEN_SQUARE_BRACKET: + if (lastCharacter !== ESCAPE_CHARACTER) { return this.compileInternal(formatString.slice(1), true, false, char); } break; - case '{': - if (lastCharacter !== '\\') { + case OPEN_CURLY_BRACKET: + if (lastCharacter !== ESCAPE_CHARACTER) { return this.compileInternal(formatString.slice(1), false, true, char); } break; - case ']': - case '}': - if (lastCharacter !== '\\') { + case CLOSE_CURLY_BRACKET: + case CLOSE_SQUARE_BRACKET: + if (lastCharacter !== ESCAPE_CHARACTER) { return this.compileInternal( formatString.slice(1), false, @@ -56,8 +71,8 @@ export default class Compiler { ); } break; - case '\\': - if (lastCharacter !== '\\') { + case ESCAPE_CHARACTER: + if (lastCharacter !== ESCAPE_CHARACTER) { return this.compileInternal( formatString.slice(1), valuable, @@ -70,38 +85,38 @@ export default class Compiler { if (valuable) { switch (char) { - case '0': + case FIXED_NUMERIC_CHARACTER: return new ValueState( this.compileInternal(formatString.substring(1), true, false, char), FIXED_STATE_TYPES.numeric ); - case 'A': + case FIXED_LITERAL_CHARACTER: return new ValueState( this.compileInternal(formatString.substring(1), true, false, char), FIXED_STATE_TYPES.literal ); - case '_': + case FIXED_ALPHA_NUMERIC_CHARACTER: return new ValueState( this.compileInternal(formatString.substring(1), true, false, char), FIXED_STATE_TYPES.alphaNumeric ); - case '…': + case ELLIPSIS_CHARACTER: // Ellipses remain elliptical: re-construct inherited type from lastCharacter return new ValueState( null, this.determineInheritedType(lastCharacter) ); - case '9': + case OPTIONAL_NUMERIC_CHARACTER: return new OptionalValueState( this.compileInternal(formatString.substring(1), true, false, char), OPTIONAL_STATE_TYPES.numeric ); - case 'a': + case OPTIONAL_LITERAL_CHARACTER: return new OptionalValueState( this.compileInternal(formatString.substring(1), true, false, char), OPTIONAL_STATE_TYPES.literal ); - case '-': + case OPTIONAL_ALPHA_NUMERIC_CHARACTER: return new OptionalValueState( this.compileInternal(formatString.substring(1), true, false, char), OPTIONAL_STATE_TYPES.alphaNumeric @@ -132,16 +147,16 @@ export default class Compiler { lastCharacter: string | null ): StateType | Notation { switch (lastCharacter) { - case '0': - case '9': + case FIXED_NUMERIC_CHARACTER: + case OPTIONAL_NUMERIC_CHARACTER: return FIXED_STATE_TYPES.numeric; - case 'A': - case 'a': + case FIXED_LITERAL_CHARACTER: + case OPTIONAL_LITERAL_CHARACTER: return FIXED_STATE_TYPES.literal; - case '_': - case '-': - case '…': - case '[': + case FIXED_ALPHA_NUMERIC_CHARACTER: + case OPTIONAL_ALPHA_NUMERIC_CHARACTER: + case ELLIPSIS_CHARACTER: + case OPEN_SQUARE_BRACKET: return FIXED_STATE_TYPES.alphaNumeric; default: return this.determineTypeWithCustomNotations(lastCharacter); diff --git a/src/web/helper/FormatSanitizer.ts b/src/web/helper/FormatSanitizer.ts index 1b76c4c..52e2356 100644 --- a/src/web/helper/FormatSanitizer.ts +++ b/src/web/helper/FormatSanitizer.ts @@ -1,4 +1,16 @@ +import { + CLOSE_CURLY_BRACKET, + CLOSE_SQUARE_BRACKET, + ESCAPE_CHARACTER, + FIXED_ALPHA_NUMERIC_CHARACTER, + FIXED_LITERAL_CHARACTER, + OPEN_CURLY_BRACKET, + OPEN_SQUARE_BRACKET, + OPTIONAL_ALPHA_NUMERIC_CHARACTER, + OPTIONAL_LITERAL_CHARACTER, +} from '../model/constants'; import FormatError from './FormatError'; +import { sortString } from './string'; export default class FormatSanitizer { public static sanitize(formatString: string): string { @@ -15,7 +27,7 @@ export default class FormatSanitizer { let escape = false; for (const char of formatString) { - if (char === '\\') { + if (char === ESCAPE_CHARACTER) { if (!escape) { escape = true; currentBlock += char; @@ -23,7 +35,10 @@ export default class FormatSanitizer { } } - if ((char === '[' || char === '{') && !escape) { + if ( + (char === OPEN_CURLY_BRACKET || char === OPEN_SQUARE_BRACKET) && + !escape + ) { if (currentBlock.length > 0) { blocks.push(currentBlock); } @@ -32,7 +47,10 @@ export default class FormatSanitizer { currentBlock += char; - if ((char === ']' || char === '}') && !escape) { + if ( + (char === CLOSE_SQUARE_BRACKET || char === CLOSE_CURLY_BRACKET) && + !escape + ) { blocks.push(currentBlock); currentBlock = ''; } @@ -47,61 +65,51 @@ export default class FormatSanitizer { return blocks; } + private static processBlock = (block: string): string[] => { + const results: string[] = []; + let buffer = ''; + let i = 0; + + while (i < block.length) { + const char = block.charAt(i); + + if (char === CLOSE_SQUARE_BRACKET) { + buffer += char; + i++; + continue; + } + + if (char === OPEN_SQUARE_BRACKET && !buffer.endsWith(ESCAPE_CHARACTER)) { + buffer += char; + results.push(buffer); + break; + } + if ( + (/[09]/.test(char) && /[Aa_\\-]/.test(buffer)) || + (/[Aa]/.test(char) && /[0-9_\\-]/.test(buffer)) || + (/[_\\-]/.test(char) && /[0-9Aa]/.test(buffer)) + ) { + buffer += CLOSE_SQUARE_BRACKET; + results.push(buffer); + buffer = OPEN_SQUARE_BRACKET + char; + i++; + continue; + } + + buffer += char; + i++; + } + + return results.length ? results : [block]; + }; + private static divideBlocksWithMixedCharacters(blocks: string[]): string[] { const resultingBlocks: string[] = []; for (const block of blocks) { - if (block.startsWith('[')) { - let blockBuffer = ''; - for (let i = 0; i < block.length; i++) { - const blockCharacter = block[i]; - if (blockCharacter === '[') { - blockBuffer += blockCharacter; - continue; - } - if (blockCharacter === ']' && !blockBuffer.endsWith('\\')) { - blockBuffer += blockCharacter; - resultingBlocks.push(blockBuffer); - break; - } - if ( - (blockCharacter === '0' || blockCharacter === '9') && - (blockBuffer.includes('A') || - blockBuffer.includes('a') || - blockBuffer.includes('-') || - blockBuffer.includes('_')) - ) { - blockBuffer += ']'; - resultingBlocks.push(blockBuffer); - blockBuffer = '[' + blockCharacter; - continue; - } - if ( - (blockCharacter === 'A' || blockCharacter === 'a') && - (blockBuffer.includes('0') || - blockBuffer.includes('9') || - blockBuffer.includes('-') || - blockBuffer.includes('_')) - ) { - blockBuffer += ']'; - resultingBlocks.push(blockBuffer); - blockBuffer = '[' + blockCharacter; - continue; - } - if ( - (blockCharacter === '-' || blockCharacter === '_') && - (blockBuffer.includes('0') || - blockBuffer.includes('9') || - blockBuffer.includes('A') || - blockBuffer.includes('a')) - ) { - blockBuffer += ']'; - resultingBlocks.push(blockBuffer); - blockBuffer = '[' + blockCharacter; - continue; - } - blockBuffer += blockCharacter; - } + if (block.startsWith(OPEN_SQUARE_BRACKET)) { + const processedBlock = this.processBlock(block); + resultingBlocks.push(...processedBlock); } else { resultingBlocks.push(block); } @@ -111,43 +119,28 @@ export default class FormatSanitizer { } private static sortFormatBlocks(blocks: string[]): string[] { - const sortedBlocks: string[] = []; - - for (const block of blocks) { - let sortedBlock: string; - if (block.startsWith('[')) { - if ( - block.includes('0') || - block.includes('9') || - block.includes('A') || - block.includes('a') - ) { - sortedBlock = - '[' + - block.replace('[', '').replace(']', '').split('').sort().join('') + - ']'; - } else { - // For `_` or `-`, temporarily replace for sorting - sortedBlock = - '[' + - block - .replace('[', '') - .replace(']', '') - .replace('_', 'A') - .replace('-', 'a') - .split('') - .sort() - .join('') + - ']'; - sortedBlock = sortedBlock.replace('A', '_').replace('a', '-'); - } + return blocks.map((block) => { + if (block.startsWith(OPEN_SQUARE_BRACKET)) { + const isSimpleBlock = /[09Aa]/.test(block); + const preparedBlock = block + .replace(OPEN_SQUARE_BRACKET, '') + .replace(CLOSE_SQUARE_BRACKET, ''); + + const sortedBlock = isSimpleBlock + ? sortString(preparedBlock) + : sortString( + preparedBlock + .replace(FIXED_ALPHA_NUMERIC_CHARACTER, FIXED_LITERAL_CHARACTER) + .replace( + OPTIONAL_ALPHA_NUMERIC_CHARACTER, + OPTIONAL_LITERAL_CHARACTER + ) + ); + return `${OPEN_SQUARE_BRACKET}${sortedBlock}${CLOSE_SQUARE_BRACKET}`; } else { - sortedBlock = block; + return block; } - sortedBlocks.push(sortedBlock); - } - - return sortedBlocks; + }); } private static checkOpenBraces(str: string): void { @@ -156,26 +149,26 @@ export default class FormatSanitizer { let curlyBraceOpen = false; for (const char of str) { - if (char === '\\') { + if (char === ESCAPE_CHARACTER) { escape = !escape; continue; } - if (char === '[') { + if (char === OPEN_SQUARE_BRACKET) { if (squareBraceOpen) { throw new FormatError(); } squareBraceOpen = !escape; } - if (char === ']' && !escape) { + if (char === CLOSE_SQUARE_BRACKET && !escape) { squareBraceOpen = false; } - if (char === '{') { + if (char === OPEN_CURLY_BRACKET) { if (curlyBraceOpen) { throw new FormatError(); } curlyBraceOpen = !escape; } - if (char === '}' && !escape) { + if (char === CLOSE_CURLY_BRACKET && !escape) { curlyBraceOpen = false; } escape = false; diff --git a/src/web/helper/Mask.ts b/src/web/helper/Mask.ts index e217c34..aa07e47 100644 --- a/src/web/helper/Mask.ts +++ b/src/web/helper/Mask.ts @@ -9,6 +9,9 @@ import FreeState from '../model/state/FreeState'; import OptionalValueState from '../model/state/OptionalValueState'; import ValueState from '../model/state/ValueState'; import CaretStringIterator from './CaretStringIterator'; +import { OPTIONAL_LITERAL_CHARACTER } from '../model/constants'; +import { reverse } from './string'; +import AutocompletionStack from './AutoCompelitionStack'; export class Mask { private static cache: Map = new Map(); @@ -137,10 +140,10 @@ export class Mask { reversed() { return { formattedText: this.formattedText.reversed(), - extractedValue: this.extractedValue.split('').reverse().join(''), + extractedValue: reverse(this.extractedValue), affinity: this.affinity, complete: this.complete, - tailPlaceholder: this.tailPlaceholder.split('').reverse().join(''), + tailPlaceholder: reverse(this.tailPlaceholder), reversed: this.reversed, }; }, @@ -155,65 +158,48 @@ export class Mask { placeholder: () => string = () => this.appendPlaceholder(this.initialState, ''); - acceptableTextLength(): number { - let state: State | null = this.initialState; + private computeLength(includeOptional: boolean, onlyValue: boolean): number { let length = 0; - while (state && !(state instanceof EOLState)) { - if ( - state instanceof FixedState || - state instanceof FreeState || - state instanceof ValueState - ) { - length += 1; + let current: State | null = this.initialState; + + while (current && !(current instanceof EOLState)) { + if (onlyValue) { + if ( + current instanceof ValueState || + current instanceof FixedState || + (includeOptional && current instanceof OptionalValueState) + ) { + length++; + } + } else { + if ( + current instanceof FreeState || + current instanceof FixedState || + current instanceof ValueState || + (includeOptional && current instanceof OptionalValueState) + ) { + length++; + } } - state = state.child; + current = current.child; } return length; } + acceptableTextLength(): number { + return this.computeLength(false, false); + } + totalTextLength(): number { - let state: State | null = this.initialState; - let length = 0; - while (state && !(state instanceof EOLState)) { - if ( - state instanceof FixedState || - state instanceof FreeState || - state instanceof ValueState || - state instanceof OptionalValueState - ) { - length += 1; - } - state = state.child; - } - return length; + return this.computeLength(true, false); } acceptableValueLength(): number { - let state: State | null = this.initialState; - let length = 0; - while (state && !(state instanceof EOLState)) { - if (state instanceof FixedState || state instanceof ValueState) { - length += 1; - } - state = state.child; - } - return length; + return this.computeLength(false, true); } totalValueLength(): number { - let state: State | null = this.initialState; - let length = 0; - while (state && !(state instanceof EOLState)) { - if ( - state instanceof FixedState || - state instanceof ValueState || - state instanceof OptionalValueState - ) { - length += 1; - } - state = state.child; - } - return length; + return this.computeLength(true, true); } private appendPlaceholder(state: State | null, placeholder: string): string { @@ -236,9 +222,15 @@ export class Mask { if ('name' in state.stateType) { switch (state.stateType.name) { case StateName.alphaNumeric: - return this.appendPlaceholder(state.child, placeholder + '-'); + return this.appendPlaceholder( + state.child, + placeholder + OPTIONAL_LITERAL_CHARACTER + ); case StateName.literal: - return this.appendPlaceholder(state.child, placeholder + 'a'); + return this.appendPlaceholder( + state.child, + placeholder + OPTIONAL_LITERAL_CHARACTER + ); case StateName.numeric: return this.appendPlaceholder(state.child, placeholder + '0'); case 'ellipsis': @@ -268,22 +260,4 @@ export class Mask { } } -class AutocompletionStack extends Array { - push(item: Next | null): number { - if (item == null) { - this.length = 0; - return 0; - } - return super.push(item); - } - - pop(): Next { - return super.pop()!; - } - - empty(): boolean { - return this.length === 0; - } -} - export default Mask; diff --git a/src/web/helper/RTLMask.ts b/src/web/helper/RTLMask.ts index ac3799a..d88c01a 100644 --- a/src/web/helper/RTLMask.ts +++ b/src/web/helper/RTLMask.ts @@ -4,6 +4,12 @@ import RTLCaretStringIterator from './RTLCaretStringIterator'; import CaretStringIterator from './CaretStringIterator'; import type { Notation } from '../../types'; import type { MaskResult } from '../model/types'; +import { + CLOSE_CURLY_BRACKET, + CLOSE_SQUARE_BRACKET, + OPEN_CURLY_BRACKET, + OPEN_SQUARE_BRACKET, +} from '../model/constants'; export default class RTLMask extends Mask { private static rtlCache: Map = new Map(); @@ -34,14 +40,14 @@ export default class RTLMask extends Mask { private static reversedFormat(format: string): string { const mapped = format.split('').reduceRight((acc, char) => { switch (char) { - case '[': - return acc + ']'; - case ']': - return acc + '['; - case '{': - return acc + '}'; - case '}': - return acc + '{'; + case OPEN_SQUARE_BRACKET: + return acc + CLOSE_SQUARE_BRACKET; + case CLOSE_SQUARE_BRACKET: + return acc + OPEN_SQUARE_BRACKET; + case OPEN_CURLY_BRACKET: + return acc + CLOSE_CURLY_BRACKET; + case CLOSE_CURLY_BRACKET: + return acc + OPEN_CURLY_BRACKET; default: return acc + char; } diff --git a/src/web/helper/string.ts b/src/web/helper/string.ts new file mode 100644 index 0000000..5b87005 --- /dev/null +++ b/src/web/helper/string.ts @@ -0,0 +1,5 @@ +export const sortString = (string: string): string => + string.split('').sort().join(''); + +export const reverse = (str: string): string => + str.split('').reverse().join(''); diff --git a/src/web/model/CaretString.ts b/src/web/model/CaretString.ts index 4030ce4..f685d41 100644 --- a/src/web/model/CaretString.ts +++ b/src/web/model/CaretString.ts @@ -1,3 +1,4 @@ +import { reverse } from '../helper/string'; import type { CaretGravity } from './types'; class CaretString { @@ -15,13 +16,12 @@ class CaretString { this.caretGravity = caretGravity; } - reversed(): CaretString { - return new CaretString( - this.string.split('').reverse().join(''), + reversed = (): CaretString => + new CaretString( + reverse(this.string), this.string.length - this.caretPosition, this.caretGravity ); - } } export default CaretString; diff --git a/src/web/model/constants.ts b/src/web/model/constants.ts index 94d658d..8f10481 100644 --- a/src/web/model/constants.ts +++ b/src/web/model/constants.ts @@ -35,3 +35,25 @@ export const FIXED_STATE_TYPES: Record = { typeString: '[_]', }, }; + +export const ELLIPSES = 'ellipsis'; +export const NULL_STRING = 'null'; + +export const OPEN_SQUARE_BRACKET = '['; +export const CLOSE_SQUARE_BRACKET = ']'; + +export const OPEN_CURLY_BRACKET = '{'; +export const CLOSE_CURLY_BRACKET = '}'; + +export const ESCAPE_CHARACTER = '\\'; + +export const OPTIONAL_NUMERIC_CHARACTER = '9'; +export const FIXED_NUMERIC_CHARACTER = '0'; + +export const FIXED_LITERAL_CHARACTER = 'A'; +export const OPTIONAL_LITERAL_CHARACTER = 'a'; + +export const FIXED_ALPHA_NUMERIC_CHARACTER = '_'; +export const OPTIONAL_ALPHA_NUMERIC_CHARACTER = '-'; + +export const ELLIPSIS_CHARACTER = '…'; diff --git a/src/web/model/state/FixedState.ts b/src/web/model/state/FixedState.ts index b4114d8..008a43e 100644 --- a/src/web/model/state/FixedState.ts +++ b/src/web/model/state/FixedState.ts @@ -1,3 +1,4 @@ +import { NULL_STRING } from '../constants'; import type { Next } from '../types'; import State from './State'; @@ -32,7 +33,7 @@ class FixedState extends State { }); toString = () => - `{${this.ownCharacter}} -> ${this.child?.toString() ?? 'null'} `; + `{${this.ownCharacter}} -> ${this.child?.toString() ?? NULL_STRING} `; } export default FixedState; diff --git a/src/web/model/state/FreeState.ts b/src/web/model/state/FreeState.ts index 5a41d27..94ec2e2 100644 --- a/src/web/model/state/FreeState.ts +++ b/src/web/model/state/FreeState.ts @@ -1,5 +1,6 @@ import State from './State'; import type { Next } from '../types'; +import { NULL_STRING } from '../constants'; class FreeState extends State { ownCharacter: string; @@ -9,7 +10,7 @@ class FreeState extends State { this.ownCharacter = ownCharacter; } - accept: (char: string) => Next | null = (char: string): Next | null => { + accept = (char: string): Next | null => { return this.ownCharacter === char ? { state: this.nextState(), @@ -25,15 +26,17 @@ class FreeState extends State { }; }; - autocomplete: () => Next | null = () => ({ + autocomplete = (): Next | null => ({ state: this.nextState(), insert: this.ownCharacter, pass: false, value: null, }); - toString: () => string = () => - `${this.ownCharacter} -> ${this.child ? this.child.toString() : 'null'}`; + toString = (): string => + `${this.ownCharacter} -> ${ + this.child ? this.child.toString() : NULL_STRING + }`; } export default FreeState; diff --git a/src/web/model/state/OptionalValueState.ts b/src/web/model/state/OptionalValueState.ts index 7adc6ef..4a12aba 100644 --- a/src/web/model/state/OptionalValueState.ts +++ b/src/web/model/state/OptionalValueState.ts @@ -1,4 +1,5 @@ import type { Notation } from '../../../types'; +import { NULL_STRING } from '../constants'; import type { Next, StateType } from '../types'; import { getCharacterTypeString } from '../utils'; import State from './State'; @@ -11,7 +12,7 @@ class OptionalValueState extends State { this.stateType = stateType; } - private accepts(character: string): boolean { + private accepts = (character: string): boolean => { if (this.stateType) { if ('name' in this.stateType) { return this.stateType.regex.test(character); @@ -21,9 +22,9 @@ class OptionalValueState extends State { } return false; - } + }; - accept: (character: string) => Next = (character) => + accept = (character: string): Next => this.accepts(character) ? { state: this.nextState(), @@ -33,10 +34,10 @@ class OptionalValueState extends State { } : { state: this.nextState(), insert: null, pass: false, value: null }; - toString: () => string = () => { + toString = (): string => { const typeStr = getCharacterTypeString(this.stateType); - return `${typeStr} -> ${this.child?.toString() ?? 'null'}`; + return `${typeStr} -> ${this.child?.toString() ?? NULL_STRING}`; }; } diff --git a/src/web/model/state/State.ts b/src/web/model/state/State.ts index 56611a4..cf928c6 100644 --- a/src/web/model/state/State.ts +++ b/src/web/model/state/State.ts @@ -1,3 +1,4 @@ +import { NULL_STRING } from '../constants'; import type { Next } from '../types'; abstract class State { @@ -14,7 +15,7 @@ abstract class State { autocomplete = (): Next | null => null; toString = (): string => - `BASE -> ${this.child ? this.child.toString() : 'null'}`; + `BASE -> ${this.child ? this.child.toString() : NULL_STRING}`; } export default State; diff --git a/src/web/model/state/ValueState.ts b/src/web/model/state/ValueState.ts index 2eb0c75..3057816 100644 --- a/src/web/model/state/ValueState.ts +++ b/src/web/model/state/ValueState.ts @@ -2,6 +2,7 @@ import State from './State'; import type { Ellipsis, Next, StateType } from '../types'; import type { Notation } from '../../../types'; import { getCharacterTypeString } from '../utils'; +import { ELLIPSES, NULL_STRING } from '../constants'; class ValueState extends State { stateType: StateType | Ellipsis | Notation; @@ -16,7 +17,7 @@ class ValueState extends State { private accepts(character: string): boolean { if ('name' in this.stateType) { - if (this.stateType.name === 'ellipsis') { + if (this.stateType.name === ELLIPSES) { return this.checkEllipsis(this.stateType.inheritedType, character); } @@ -25,12 +26,12 @@ class ValueState extends State { return this.stateType.characterSet.includes(character); } - private checkEllipsis( + private checkEllipsis = ( stateType: StateType | Ellipsis | Notation, character: string - ): boolean { + ): boolean => { if ('name' in stateType) { - if (stateType.name === 'ellipsis') { + if (stateType.name === ELLIPSES) { this.checkEllipsis(stateType.inheritedType, character); } else { return stateType.regex.test(character); @@ -38,7 +39,7 @@ class ValueState extends State { } return (stateType as Notation).characterSet.includes(character); - } + }; accept: (character: string) => Next | null = (character) => this.accepts(character) @@ -51,7 +52,7 @@ class ValueState extends State { : null; get isElliptical(): boolean { - return 'name' in this.stateType && this.stateType.name === 'ellipsis'; + return 'name' in this.stateType && this.stateType.name === ELLIPSES; } nextState: () => State = () => (this.isElliptical ? this : this.child!); @@ -59,7 +60,7 @@ class ValueState extends State { toString: () => string = () => { const typeStr = getCharacterTypeString(this.stateType); - return `${typeStr} -> ${this.child?.toString() ?? 'null'}`; + return `${typeStr} -> ${this.child?.toString() ?? NULL_STRING}`; }; }