{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "regex-input-incremental", "type": "registry:component", "registryDependencies": [ "input" ], "files": [ { "path": "components/domain-ui/regex-input-incremental.tsx", "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { \n useIncrementalRegex,\n type ValidationStatusType \n} from \"@/hooks/use-incremental-regex\";\n\nexport interface RegexInputIncrementalProps\n extends React.ComponentProps<\"input\"> {\n regex: RegExp;\n onValidation?: (status: ValidationStatusType) => void;\n transformToUppercase?: boolean;\n}\n\nexport function RegexInputIncremental({\n regex,\n value,\n onChange,\n onValidation,\n defaultValue,\n transformToUppercase,\n ...props\n}: RegexInputIncrementalProps) {\n const {\n value: displayValue,\n onChange: handleChange,\n onPaste: handlePaste,\n } = useIncrementalRegex({\n regex,\n value: value as string | undefined,\n onChange: onChange\n ? (newValue: string) => {\n const syntheticEvent = {\n target: { value: newValue },\n currentTarget: { value: newValue },\n } as React.ChangeEvent;\n onChange(syntheticEvent);\n }\n : undefined,\n onValidation,\n defaultValue: defaultValue as string | undefined,\n transformToUppercase,\n });\n\n return (\n \n );\n}\n", "type": "registry:component" }, { "path": "hooks/use-incremental-regex.ts", "content": "\"use client\";\n\nimport { useState, useCallback, useMemo, useRef, useEffect } from \"react\";\nimport {\n validateIncrementalInput,\n matches,\n canPartiallyMatch,\n} from \"../lib/incremental-regex\";\n\n/**\n * React Hook for Incremental Regex Input Validation\n *\n * Provides configurable real-time regex validation that:\n * - Accepts partial input that could eventually match\n * - Rejects characters that make matching impossible\n * - Works with any JavaScript RegExp, including complex alternation patterns\n * - Supports paste detection and full validation\n * - Provides error states and validation control\n *\n * Uses NFA-based incremental matching for proper regex support.\n */\n\nexport const ValidationStatus = {\n Valid: \"valid\",\n Incomplete: \"incomplete\",\n Invalid: \"invalid\",\n} as const;\n\nexport type ValidationStatusType = typeof ValidationStatus[keyof typeof ValidationStatus];\n\nexport interface UseIncrementalRegexProps {\n regex: RegExp;\n value?: string;\n onChange?: (value: string) => void;\n onValidation?: (status: ValidationStatusType) => void;\n defaultValue?: string;\n transformToUppercase?: boolean;\n}\n\nexport interface UseIncrementalRegexReturn {\n value: string;\n onChange: (e: React.ChangeEvent) => void;\n onPaste: () => void;\n isValid: boolean;\n validationStatus: ValidationStatusType;\n}\n\nexport const useIncrementalRegex = ({\n regex,\n value: controlledValue,\n onChange,\n onValidation,\n defaultValue,\n transformToUppercase,\n}: UseIncrementalRegexProps): UseIncrementalRegexReturn => {\n // Internal configuration - skip validation for defaults\n const skipLiveValidationForDefaults = true;\n // Initialize with processed default value\n const processedDefaultValue = useMemo(() => {\n if (!defaultValue) {\n return \"\";\n }\n\n // If skipLiveValidationForDefaults is false, validate the default value\n if (!skipLiveValidationForDefaults) {\n return validateIncrementalInput(defaultValue, regex);\n }\n\n return defaultValue;\n }, [defaultValue, regex]);\n\n const [uncontrolledValue, setUncontrolledValue] = useState(\n processedDefaultValue\n );\n const wasDefaultValueSet = useRef(!!defaultValue);\n\n // Use controlled value if provided, otherwise use internal state\n const value =\n controlledValue !== undefined ? controlledValue : uncontrolledValue;\n\n // Validation state computation\n const { isValid, validationStatus } = useMemo(() => {\n const isFullMatch = matches(value, regex);\n const canPartialMatch =\n value.length > 0 ? canPartiallyMatch(value, regex) : true;\n\n let status: ValidationStatusType;\n if (isFullMatch) {\n status = ValidationStatus.Valid;\n } else if (canPartialMatch) {\n status = ValidationStatus.Incomplete;\n } else {\n status = ValidationStatus.Invalid;\n }\n\n return {\n isValid: isFullMatch,\n validationStatus: status,\n };\n }, [value, regex]);\n\n // Notify validation changes\n useEffect(() => {\n if (onValidation) {\n onValidation(validationStatus);\n }\n }, [validationStatus, onValidation]);\n\n // Track if we're in a paste operation\n const isPasteRef = useRef(false);\n\n // Handle regular input changes\n const handleChange = useCallback(\n (e: React.ChangeEvent) => {\n let newValue = e.target.value;\n \n // Transform to uppercase if requested\n if (transformToUppercase) {\n newValue = newValue.toUpperCase();\n }\n \n let processedValue = newValue;\n\n // If this was triggered by a paste event, accept the full value\n // Otherwise, validate incrementally\n if (isPasteRef.current) {\n // Accept the full pasted value even if invalid\n processedValue = newValue;\n isPasteRef.current = false; // Reset the flag\n } else {\n // For typing, validate incrementally - keep only valid prefix\n processedValue = validateIncrementalInput(newValue, regex);\n }\n\n // Clear default value flag after first interaction\n if (wasDefaultValueSet.current) {\n wasDefaultValueSet.current = false;\n }\n\n // Update state if uncontrolled\n if (controlledValue === undefined) {\n setUncontrolledValue(processedValue);\n }\n\n // Call onChange callback with processed value\n if (onChange) {\n onChange(processedValue);\n }\n },\n [regex, controlledValue, onChange, transformToUppercase]\n );\n\n // Handle paste events - just set a flag\n const handlePaste = useCallback(() => {\n isPasteRef.current = true;\n }, []);\n\n return {\n value,\n onChange: handleChange,\n onPaste: handlePaste,\n isValid,\n validationStatus,\n };\n};\n", "type": "registry:hook" }, { "path": "lib/incremental-regex.ts", "content": "\"use client\";\n\nimport { parseRegExpLiteral } from \"@eslint-community/regexpp\";\nimport { NFABuilder } from \"./nfa-builder\";\nimport { NFA } from \"./nfa\";\n\n/**\n * Incremental Regular Expression Matcher\n *\n * Converts JavaScript RegExp to NFA and provides incremental matching:\n * - `canPartiallyMatch(input)`: Can this partial input eventually match?\n * - `matches(input)`: Does this complete input match?\n *\n * Uses AST parsing + Thompson's NFA construction for proper regex support.\n */\n\nclass IncrementalRegexMatcher {\n private nfa: NFA;\n private originalRegex: RegExp;\n\n constructor(regex: RegExp) {\n this.originalRegex = regex;\n\n try {\n // Parse regex to AST using well-maintained regexpp\n // parseRegExpLiteral expects format: /pattern/flags\n const regexLiteral = `/${regex.source}/${regex.flags}`;\n const ast = parseRegExpLiteral(regexLiteral);\n\n // Build NFA using Thompson's construction\n const builder = new NFABuilder();\n this.nfa = builder.build(ast.pattern);\n } catch (_error) {\n // Fallback: if parsing fails, create a permissive NFA\n // Silently fall back to permissive NFA if parsing fails\n this.nfa = this.createFallbackNFA();\n }\n }\n\n /**\n * Check if partial input could eventually match the regex\n */\n canPartiallyMatch(input: string): boolean {\n // First try NFA-based matching\n try {\n return this.nfa.canPartiallyMatch(input);\n } catch (_error) {\n // Fallback to original regex with brute force extension\n // Silently fall back to brute force matching if NFA fails\n return this.fallbackCanMatch(input);\n }\n }\n\n /**\n * Check if complete input matches the regex\n */\n matches(input: string): boolean {\n // Use original regex for exact matching\n return this.originalRegex.test(input);\n }\n\n /**\n * Create simple fallback NFA that accepts any input\n */\n private createFallbackNFA(): NFA {\n // Simple NFA: state 0 -> state 1 on any character, with loop back\n return new NFA(\n 0,\n new Set([0, 1]),\n [\n { from: 0, to: 1, symbol: \".\" }, // Any character\n { from: 1, to: 1, symbol: \".\" }, // Loop back\n ],\n 2\n );\n }\n\n /**\n * Brute force fallback for partial matching\n */\n private fallbackCanMatch(input: string): boolean {\n // Try extending input with common characters\n const commonChars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n\n for (const char of commonChars) {\n if (this.originalRegex.test(input + char)) {\n return true;\n }\n }\n\n // Try extending with multiple characters\n for (let len = 2; len <= 5; len++) {\n const extension = \"0\".repeat(len);\n if (this.originalRegex.test(input + extension)) {\n return true;\n }\n }\n\n return false;\n }\n}\n\n// Cache for regex matchers to avoid rebuilding NFAs\nconst matcherCache = new Map();\n\n/**\n * Get or create incremental matcher for a regex\n */\nfunction getIncrementalMatcher(regex: RegExp): IncrementalRegexMatcher {\n const key = `${regex.source}_${regex.flags}`;\n\n if (!matcherCache.has(key)) {\n matcherCache.set(key, new IncrementalRegexMatcher(regex));\n }\n\n const matcher = matcherCache.get(key);\n if (!matcher) {\n throw new Error(`Matcher not found for regex: ${regex.source}`);\n }\n return matcher;\n}\n\n/**\n * Check if partial input could eventually match regex\n */\nexport function canPartiallyMatch(input: string, regex: RegExp): boolean {\n const matcher = getIncrementalMatcher(regex);\n return matcher.canPartiallyMatch(input);\n}\n\n/**\n * Check if complete input matches regex\n */\nexport function matches(input: string, regex: RegExp): boolean {\n const matcher = getIncrementalMatcher(regex);\n return matcher.matches(input);\n}\n\n/**\n * Validate input character by character, keeping only valid prefixes\n */\nexport function validateIncrementalInput(input: string, regex: RegExp): string {\n let validInput = \"\";\n\n for (const char of input) {\n const testInput = validInput + char;\n\n if (canPartiallyMatch(testInput, regex)) {\n validInput = testInput;\n } else {\n // Stop at first invalid character\n break;\n }\n }\n\n return validInput;\n}\n", "type": "registry:lib" }, { "path": "lib/nfa.ts", "content": "\"use client\";\n\n/**\n * Minimal NFA (Non-deterministic Finite Automaton) Implementation\n * for Incremental Regular Expression Matching\n *\n * Based on Thompson's construction algorithm:\n * - Characters create simple transitions\n * - Concatenation connects NFAs sequentially\n * - Alternation creates epsilon branches\n * - Quantifiers create epsilon loops/choices\n */\n\nexport interface NFATransition {\n from: number;\n to: number;\n symbol: string | null; // null represents epsilon (ε) transition\n}\n\nexport class NFA {\n readonly startState: number;\n readonly acceptStates: Set;\n readonly transitions: NFATransition[];\n readonly stateCount: number;\n\n constructor(\n startState: number,\n acceptStates: Set,\n transitions: NFATransition[],\n stateCount: number\n ) {\n this.startState = startState;\n this.acceptStates = acceptStates;\n this.transitions = transitions;\n this.stateCount = stateCount;\n }\n\n /**\n * Compute epsilon closure of a set of states\n * (all states reachable via epsilon transitions)\n */\n private epsilonClosure(states: Set): Set {\n const closure = new Set(states);\n const stack = Array.from(states);\n\n while (stack.length > 0) {\n const state = stack.pop();\n if (state === undefined) {\n break;\n }\n\n // Find all epsilon transitions from this state\n for (const transition of this.transitions) {\n if (\n transition.from === state &&\n transition.symbol === null &&\n !closure.has(transition.to)\n ) {\n closure.add(transition.to);\n stack.push(transition.to);\n }\n }\n }\n\n return closure;\n }\n\n /**\n * Get all states reachable from given states on a specific symbol\n */\n private move(states: Set, symbol: string): Set {\n const nextStates = new Set();\n\n for (const state of states) {\n for (const transition of this.transitions) {\n if (transition.from === state && transition.symbol === symbol) {\n nextStates.add(transition.to);\n }\n }\n }\n\n return nextStates;\n }\n\n /**\n * Check if partial input could eventually match the regex\n * Returns true if there's a path from current states to accept states\n */\n canPartiallyMatch(input: string): boolean {\n // Start with epsilon closure of start state\n let currentStates = this.epsilonClosure(new Set([this.startState]));\n\n // Process each character in input\n for (const char of input) {\n // Move on character and compute epsilon closure\n const nextStates = this.move(currentStates, char);\n currentStates = this.epsilonClosure(nextStates);\n\n // If no states reachable, input cannot match\n if (currentStates.size === 0) {\n return false;\n }\n }\n\n // For partial matching, we just need to have reachable states\n // The input doesn't need to be complete\n return currentStates.size > 0;\n }\n\n /**\n * Check if input fully matches the regex\n */\n matches(input: string): boolean {\n let currentStates = this.epsilonClosure(new Set([this.startState]));\n\n for (const char of input) {\n const nextStates = this.move(currentStates, char);\n currentStates = this.epsilonClosure(nextStates);\n\n if (currentStates.size === 0) {\n return false;\n }\n }\n\n // For full match, we need to be in an accept state\n for (const state of currentStates) {\n if (this.acceptStates.has(state)) {\n return true;\n }\n }\n\n return false;\n }\n}\n", "type": "registry:lib" }, { "path": "lib/nfa-builder.ts", "content": "\"use client\";\n\nimport type * as RegExpAST from \"@eslint-community/regexpp/ast\";\nimport { NFA, type NFATransition } from \"./nfa\";\n\n/**\n * NFA Builder using Thompson's Construction Algorithm\n *\n * Converts regex AST nodes into NFA fragments and combines them\n * according to Thompson's construction rules:\n *\n * 1. Character: state1 --char--> state2\n * 2. Concatenation: NFA1 -> NFA2 (connect accept of NFA1 to start of NFA2)\n * 3. Alternation: state --ε--> NFA1 --ε--> accept\n * \\--ε--> NFA2 --ε--> accept\n * 4. Quantifiers: add epsilon loops and optional paths\n */\n\ninterface NFAFragment {\n startState: number;\n acceptState: number;\n transitions: NFATransition[];\n}\n\nexport class NFABuilder {\n private stateCounter = 0;\n\n private nextState(): number {\n return this.stateCounter++;\n }\n\n /**\n * Build NFA from regex AST\n */\n build(ast: RegExpAST.Pattern): NFA {\n this.stateCounter = 0;\n\n // Check if the pattern has multiple alternatives at the top level\n const alternatives = ast.body || ast.alternatives;\n if (!alternatives || alternatives.length === 0) {\n throw new Error(\"No alternatives found in regex AST\");\n }\n\n let fragment: NFAFragment;\n if (alternatives.length === 1) {\n // Single alternative\n fragment = this.buildFragment(alternatives[0]);\n } else {\n // Multiple alternatives - create a disjunction\n const disjunction = {\n type: \"Disjunction\" as const,\n alternatives,\n };\n fragment = this.buildDisjunction(disjunction);\n }\n\n return new NFA(\n fragment.startState,\n new Set([fragment.acceptState]),\n fragment.transitions,\n this.stateCounter\n );\n }\n\n /**\n * Build NFA fragment from AST node\n */\n private buildFragment(node: RegExpAST.Alternative): NFAFragment {\n // Handle different property names - newer versions use 'elements'\n const elements = node.body || node.elements;\n\n if (!elements || elements.length === 0) {\n // Empty alternative - epsilon transition\n const start = this.nextState();\n const accept = this.nextState();\n return {\n startState: start,\n acceptState: accept,\n transitions: [{ from: start, to: accept, symbol: null }],\n };\n }\n\n // Build fragments for each element and concatenate\n let current = this.buildElementFragment(elements[0]);\n\n for (let i = 1; i < elements.length; i++) {\n const next = this.buildElementFragment(elements[i]);\n current = this.concatenate(current, next);\n }\n\n return current;\n }\n\n /**\n * Build fragment from individual AST elements\n */\n private buildElementFragment(element: RegExpAST.Element): NFAFragment {\n switch (element.type) {\n case \"Character\":\n return this.buildCharacter(String.fromCharCode(element.value));\n\n case \"CharacterClass\":\n return this.buildCharacterClass(element);\n\n case \"CharacterSet\":\n // CharacterSet is another name for CharacterClass in newer versions\n return this.buildCharacterClass(element as RegExpAST.CharacterClass);\n\n case \"Quantifier\":\n return this.buildQuantifier(element);\n\n case \"Group\": {\n // Groups can contain disjunctions - handle both single alternatives and disjunctions\n if (element.alternatives && element.alternatives.length > 1) {\n // This is a disjunction inside a group - create disjunction structure\n const disjunction = {\n type: \"Disjunction\" as const,\n alternatives: element.alternatives,\n };\n return this.buildDisjunction(disjunction);\n }\n // Single alternative in group\n const groupAlternative = element.body?.[0] || element.alternatives?.[0];\n return this.buildFragment(groupAlternative);\n }\n\n case \"Disjunction\":\n return this.buildDisjunction(element);\n\n case \"Assertion\":\n // Assertions (^, $, \\b, etc.) are epsilon transitions - they don't consume input\n return this.buildEpsilonFragment();\n\n case \"CapturingGroup\": {\n // CapturingGroup is similar to Group but with capturing semantics\n // For NFA purposes, treat it the same as Group\n if (element.alternatives && element.alternatives.length > 1) {\n const disjunction = {\n type: \"Disjunction\" as const,\n alternatives: element.alternatives,\n };\n return this.buildDisjunction(disjunction);\n }\n const groupAlternative = element.body?.[0] || element.alternatives?.[0];\n return this.buildFragment(groupAlternative);\n }\n\n default:\n // Fallback for unsupported elements - epsilon transition to be safe\n // Silently handle unsupported element types\n return this.buildEpsilonFragment();\n }\n }\n\n /**\n * Build epsilon fragment: start --ε--> accept\n */\n private buildEpsilonFragment(): NFAFragment {\n const start = this.nextState();\n const accept = this.nextState();\n\n return {\n startState: start,\n acceptState: accept,\n transitions: [{ from: start, to: accept, symbol: null }],\n };\n }\n\n /**\n * Build character fragment: start --char--> accept\n */\n private buildCharacter(char: string): NFAFragment {\n const start = this.nextState();\n const accept = this.nextState();\n\n return {\n startState: start,\n acceptState: accept,\n transitions: [{ from: start, to: accept, symbol: char }],\n };\n }\n\n /**\n * Build character class fragment (e.g., [A-Z], [0-9])\n */\n private buildCharacterClass(\n charClass: RegExpAST.CharacterClass\n ): NFAFragment {\n const start = this.nextState();\n const accept = this.nextState();\n const transitions: NFATransition[] = [];\n\n // Handle both body and elements properties\n const elements = charClass.body || charClass.elements || [];\n\n for (const element of elements) {\n switch (element.type) {\n case \"Character\":\n transitions.push({\n from: start,\n to: accept,\n symbol: String.fromCharCode(element.value),\n });\n break;\n\n case \"CharacterClassRange\":\n // Add transition for each character in range\n for (\n let code = element.min.value;\n code <= element.max.value;\n code++\n ) {\n transitions.push({\n from: start,\n to: accept,\n symbol: String.fromCharCode(code),\n });\n }\n break;\n\n default:\n // Skip unsupported character class elements\n break;\n }\n }\n\n return { startState: start, acceptState: accept, transitions };\n }\n\n /**\n * Build quantifier fragment (*, +, ?, {n,m})\n */\n private buildQuantifier(quantifier: RegExpAST.Quantifier): NFAFragment {\n const { min, max } = quantifier;\n\n // For exact repetitions like {8} or {7}, build a chain\n if (min === max && min > 0) {\n return this.buildExactRepetition(quantifier.element, min);\n }\n\n // For other quantifiers, use the general approach\n const innerFragment = this.buildElementFragment(quantifier.element);\n const start = this.nextState();\n const accept = this.nextState();\n\n const transitions: NFATransition[] = [...innerFragment.transitions];\n\n if (min === 0) {\n // Optional (?, *, {0,n}) - epsilon from start to accept\n transitions.push({ from: start, to: accept, symbol: null });\n }\n\n if (max === null || max > 1) {\n // Repeatable (+, *, {n,}) - epsilon loop back\n transitions.push({\n from: innerFragment.acceptState,\n to: innerFragment.startState,\n symbol: null,\n });\n }\n\n // Connect start to inner fragment\n transitions.push({\n from: start,\n to: innerFragment.startState,\n symbol: null,\n });\n\n // Connect inner fragment to accept\n transitions.push({\n from: innerFragment.acceptState,\n to: accept,\n symbol: null,\n });\n\n return { startState: start, acceptState: accept, transitions };\n }\n\n /**\n * Build exact repetition fragment for quantifiers like {8}\n */\n private buildExactRepetition(\n element: RegExpAST.Element,\n count: number\n ): NFAFragment {\n if (count === 0) {\n return this.buildEpsilonFragment();\n }\n\n // Build first fragment\n let current = this.buildElementFragment(element);\n\n // Chain additional fragments\n for (let i = 1; i < count; i++) {\n const next = this.buildElementFragment(element);\n current = this.concatenate(current, next);\n }\n\n return current;\n }\n\n /**\n * Build disjunction (alternation) fragment: a|b\n */\n private buildDisjunction(disjunction: RegExpAST.Disjunction): NFAFragment {\n const start = this.nextState();\n const accept = this.nextState();\n const transitions: NFATransition[] = [];\n\n // Connect start to each alternative via epsilon\n // Connect each alternative to accept via epsilon\n for (const alternative of disjunction.alternatives) {\n const fragment = this.buildFragment(alternative);\n\n // start --ε--> fragment.start\n transitions.push({ from: start, to: fragment.startState, symbol: null });\n\n // fragment.accept --ε--> accept\n transitions.push({\n from: fragment.acceptState,\n to: accept,\n symbol: null,\n });\n\n // Add fragment's transitions\n transitions.push(...fragment.transitions);\n }\n\n return { startState: start, acceptState: accept, transitions };\n }\n\n /**\n * Concatenate two NFA fragments\n */\n private concatenate(first: NFAFragment, second: NFAFragment): NFAFragment {\n const transitions = [\n ...first.transitions,\n ...second.transitions,\n // Connect first's accept to second's start via epsilon\n { from: first.acceptState, to: second.startState, symbol: null },\n ];\n\n return {\n startState: first.startState,\n acceptState: second.acceptState,\n transitions,\n };\n }\n}\n", "type": "registry:lib" } ], "meta": { "tags": [ "input", "regex", "validation", "form", "incremental", "nfa" ] } }