├── index.js └── parser.js /index.js: -------------------------------------------------------------------------------- 1 | const parse = require('./parser'); 2 | 3 | function stateMatchesStringAtIndex(state, str, i) { 4 | if (i >= str.length) { 5 | return [false, 0]; 6 | } 7 | 8 | if (state.type === 'wildcard') { 9 | return [true, 1]; 10 | } 11 | 12 | if (state.type === 'element') { 13 | const match = state.value === str[i]; 14 | return [match, match ? 1 : 0]; 15 | } 16 | 17 | if (state.type === 'groupElement') { 18 | return test(state.states, str.slice(i)); 19 | } 20 | 21 | throw new Error('Unsupported element type'); 22 | } 23 | 24 | 25 | function test(states, str) { 26 | const queue = states.slice(); 27 | let i = 0; 28 | const backtrackStack = []; 29 | let currentState = queue.shift(); 30 | 31 | const backtrack = () => { 32 | queue.unshift(currentState); 33 | 34 | let couldBacktrack = false; 35 | 36 | while (backtrackStack.length) { 37 | const {isBacktrackable, state, consumptions} = backtrackStack.pop(); 38 | 39 | if (isBacktrackable) { 40 | if (consumptions.length === 0) { 41 | queue.unshift(state); 42 | continue; 43 | } 44 | const n = consumptions.pop(); 45 | i -= n; 46 | backtrackStack.push({ isBacktrackable, state, consumptions }); 47 | couldBacktrack = true; 48 | break; 49 | } 50 | 51 | queue.unshift(state); 52 | consumptions.forEach(n => { 53 | i -= n; 54 | }); 55 | } 56 | 57 | if (couldBacktrack) { 58 | currentState = queue.shift(); 59 | } 60 | 61 | return couldBacktrack; 62 | } 63 | 64 | while (currentState) { 65 | switch (currentState.quantifier) { 66 | case 'exactlyOne': { 67 | const [isMatch, consumed] = stateMatchesStringAtIndex(currentState, str, i); 68 | 69 | if (!isMatch) { 70 | const indexBeforeBacktracking = i; 71 | const couldBacktrack = backtrack(); 72 | if (!couldBacktrack) { 73 | return [false, indexBeforeBacktracking]; 74 | } 75 | continue; 76 | } 77 | 78 | backtrackStack.push({ 79 | isBacktrackable: false, 80 | state: currentState, 81 | consumptions: [ consumed ] 82 | }); 83 | i += consumed; 84 | currentState = queue.shift(); 85 | continue; 86 | } 87 | 88 | case 'zeroOrOne': { 89 | steps++; 90 | if (i >= str.length) { 91 | backtrackStack.push({ 92 | isBacktrackable: false, 93 | state: currentState, 94 | consumptions: [0] 95 | }); 96 | currentState = queue.shift(); 97 | continue; 98 | } 99 | 100 | const [isMatch, consumed] = stateMatchesStringAtIndex(currentState, str, i); 101 | i += consumed; 102 | backtrackStack.push({ 103 | isBacktrackable: isMatch && consumed > 0, 104 | state: currentState, 105 | consumptions: [ consumed ] 106 | }); 107 | currentState = queue.shift(); 108 | continue; 109 | } 110 | 111 | case 'zeroOrMore': { 112 | const backtrackState = { 113 | isBacktrackable: true, 114 | state: currentState, 115 | consumptions: [] 116 | }; 117 | 118 | while (true) { 119 | steps++; 120 | if (i >= str.length) { 121 | if (backtrackState.consumptions.length === 0) { 122 | backtrackState.consumptions.push(0); 123 | backtrackState.isBacktrackable = false; 124 | } 125 | backtrackStack.push(backtrackState); 126 | currentState = queue.shift(); 127 | break; 128 | } 129 | 130 | const [isMatch, consumed] = stateMatchesStringAtIndex(currentState, str, i); 131 | if (!isMatch || consumed === 0) { 132 | if (backtrackState.consumptions.length === 0) { 133 | backtrackState.consumptions.push(0); 134 | backtrackState.isBacktrackable = false; 135 | } 136 | backtrackStack.push(backtrackState); 137 | currentState = queue.shift(); 138 | break; 139 | } 140 | 141 | backtrackState.consumptions.push(consumed); 142 | i += consumed; 143 | } 144 | continue; 145 | } 146 | 147 | default: { 148 | currentState; 149 | throw new Error('Unsupported'); 150 | } 151 | } 152 | } 153 | 154 | return [true, i]; 155 | } 156 | 157 | const regexStr = 'a.*c'; 158 | const states = parse(regexStr); 159 | const exampleStr = 'abkjbdk!@#%$WEccccc'; 160 | console.log(test(states, exampleStr)); 161 | 162 | debugger; 163 | 164 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | // Only a small subset of regex features are used: 2 | // . wildcard 3 | // \x escaped literals (for capturing dots or parentheses) 4 | // () grouping parentheses 5 | // + greedy one or more quantifier 6 | // * greedy zero or more quantifier 7 | // ? zero or one quantifier 8 | // Everything else is considered to be a literal 9 | 10 | const last = stack => stack[stack.length-1]; 11 | function parse(re) { 12 | const stack = [[]]; 13 | let i = 0; 14 | 15 | while (i < re.length) { 16 | const next = re[i]; 17 | 18 | switch (next) { 19 | case '.': { 20 | last(stack).push({ 21 | type: 'wildcard', 22 | quantifier: 'exactlyOne' 23 | }); 24 | i++; 25 | continue; 26 | } 27 | 28 | case '\\': { 29 | if (i+1 >= re.length) { 30 | throw new Error(`Bad escape character at index ${i}`); 31 | } 32 | 33 | last(stack).push({ 34 | type: 'element', 35 | value: re[i+1], 36 | quantifier: 'exactlyOne' 37 | }); 38 | 39 | i += 2; 40 | continue; 41 | } 42 | 43 | case '(': { 44 | stack.push([]); 45 | i++ 46 | continue; 47 | } 48 | 49 | case ')': { 50 | if (stack.length <= 1) { 51 | throw new Error(`No group to close at index ${i}`); 52 | } 53 | const states = stack.pop(); 54 | last(stack).push({ 55 | type: 'groupElement', 56 | states, 57 | quantifier: 'exactlyOne' 58 | }); 59 | i++; 60 | continue; 61 | } 62 | 63 | case '?': { 64 | const lastElement = last(last(stack)); 65 | if (!lastElement || lastElement.quantifier !== 'exactlyOne') { 66 | throw new Error('Quantifer must follow an unquantified element or group'); 67 | } 68 | lastElement.quantifier = 'zeroOrOne'; 69 | i++; 70 | continue; 71 | } 72 | 73 | case '*': { 74 | const lastElement = last(last(stack)); 75 | if (!lastElement || lastElement.quantifier !== 'exactlyOne') { 76 | throw new Error('Quantifer must follow an unquantified element or group'); 77 | } 78 | lastElement.quantifier = 'zeroOrMore'; 79 | i++; 80 | continue; 81 | } 82 | 83 | case '+': { 84 | const lastElement = last(last(stack)); 85 | if (!lastElement || lastElement.quantifier !== 'exactlyOne') { 86 | throw new Error('Quantifer must follow an unquantified element or group'); 87 | } 88 | 89 | // Split this into two operations 90 | // 1. exactly one of the previous quantified element 91 | // 2. zeroOrMore of the previous quantified element 92 | const zeroOrMoreCopy = { ...lastElement, quantifier: 'zeroOrMore' }; 93 | last(stack).push(zeroOrMoreCopy); 94 | i++; 95 | continue; 96 | } 97 | 98 | default: { 99 | last(stack).push({ 100 | type: 'element', 101 | value: next, 102 | quantifier: 'exactlyOne' 103 | }); 104 | i++; 105 | continue; 106 | } 107 | } 108 | 109 | } 110 | 111 | if (stack.length !== 1) { 112 | throw new Error('Unmatched groups in regular expression'); 113 | } 114 | 115 | return stack[0]; 116 | }; 117 | 118 | module.exports = parse; 119 | --------------------------------------------------------------------------------