Mirror: The magical sticky regex-based parser generator 馃
at v3.0.2 7.6 kB view raw
1import { astRoot } from '../codegen'; 2import { parse } from '../parser'; 3 4export function makeHelpers({ types: t, template }) { 5 const regexPatternsRe = /^[()\[\]|.+?*]|[^\\][()\[\]|.+?*$^]|\\[wdsWDS]/; 6 const importSourceRe = /reghex$|^reghex\/macro/; 7 const importName = 'reghex'; 8 9 let _hasUpdatedImport = false; 10 let _matchId = t.identifier('match'); 11 let _patternId = t.identifier('__pattern'); 12 13 const _hoistedExpressions = new Map(); 14 15 return { 16 /** Adds the reghex import declaration to the Program scope */ 17 updateImport(path) { 18 if (_hasUpdatedImport) return; 19 if (!importSourceRe.test(path.node.source.value)) return; 20 _hasUpdatedImport = true; 21 22 if (path.node.source.value !== importName) { 23 path.node.source = t.stringLiteral(importName); 24 } 25 26 _patternId = path.scope.generateUidIdentifier('_pattern'); 27 path.node.specifiers.push( 28 t.importSpecifier(_patternId, t.identifier('__pattern')) 29 ); 30 31 const tagImport = path.node.specifiers.find((node) => { 32 return t.isImportSpecifier(node) && node.imported.name === 'match'; 33 }); 34 35 if (!tagImport) { 36 path.node.specifiers.push( 37 t.importSpecifier( 38 (_matchId = path.scope.generateUidIdentifier('match')), 39 t.identifier('match') 40 ) 41 ); 42 } else { 43 _matchId = tagImport.imported; 44 } 45 }, 46 47 /** Determines whether the given tagged template expression is a reghex match */ 48 isMatch(path) { 49 if ( 50 t.isTaggedTemplateExpression(path.node) && 51 t.isCallExpression(path.node.tag) && 52 t.isIdentifier(path.node.tag.callee) && 53 path.scope.hasBinding(path.node.tag.callee.name) 54 ) { 55 if (t.isVariableDeclarator(path.parentPath)) 56 path.parentPath._isMatch = true; 57 return true; 58 } 59 60 return ( 61 t.isVariableDeclarator(path.parentPath) && path.parentPath._isMatch 62 ); 63 }, 64 65 /** Given a reghex match, returns the path to reghex's match import declaration */ 66 getMatchImport(path) { 67 t.assertTaggedTemplateExpression(path.node); 68 const binding = path.scope.getBinding(path.node.tag.callee.name); 69 70 if ( 71 binding.kind !== 'module' || 72 !t.isImportDeclaration(binding.path.parent) || 73 !importSourceRe.test(binding.path.parent.source.value) || 74 !t.isImportSpecifier(binding.path.node) 75 ) { 76 return null; 77 } 78 79 return binding.path.parentPath; 80 }, 81 82 /** Given a match, returns an evaluated name or a best guess */ 83 getMatchName(path) { 84 t.assertTaggedTemplateExpression(path.node); 85 const nameArgumentPath = path.get('tag.arguments.0'); 86 if (nameArgumentPath) { 87 const { confident, value } = nameArgumentPath.evaluate(); 88 if (!confident && t.isIdentifier(nameArgumentPath.node)) { 89 return nameArgumentPath.node.name; 90 } else if (confident && typeof value === 'string') { 91 return value; 92 } 93 } 94 95 return path.scope.generateUidIdentifierBasedOnNode(path.node); 96 }, 97 98 /** Given a match, hoists its expressions in front of the match's statement */ 99 _prepareExpressions(path) { 100 t.assertTaggedTemplateExpression(path.node); 101 102 const variableDeclarators = []; 103 const matchName = this.getMatchName(path); 104 105 const hoistedExpressions = path.node.quasi.expressions.map( 106 (expression, i) => { 107 if ( 108 t.isArrowFunctionExpression(expression) && 109 t.isIdentifier(expression.body) 110 ) { 111 expression = expression.body; 112 } else if ( 113 (t.isFunctionExpression(expression) || 114 t.isArrowFunctionExpression(expression)) && 115 t.isBlockStatement(expression.body) && 116 expression.body.body.length === 1 && 117 t.isReturnStatement(expression.body.body[0]) && 118 t.isIdentifier(expression.body.body[0].argument) 119 ) { 120 expression = expression.body.body[0].argument; 121 } 122 123 const isBindingExpression = 124 t.isIdentifier(expression) && 125 path.scope.hasBinding(expression.name); 126 if (isBindingExpression) { 127 const binding = path.scope.getBinding(expression.name); 128 if (t.isVariableDeclarator(binding.path.node)) { 129 const matchPath = binding.path.get('init'); 130 if (this.isMatch(matchPath)) { 131 return expression; 132 } else if (_hoistedExpressions.has(expression.name)) { 133 return t.identifier(_hoistedExpressions.get(expression.name)); 134 } 135 } 136 } 137 138 const id = path.scope.generateUidIdentifier( 139 isBindingExpression 140 ? `${expression.name}_expression` 141 : `${matchName}_expression` 142 ); 143 144 variableDeclarators.push( 145 t.variableDeclarator( 146 id, 147 t.callExpression(t.identifier(_patternId.name), [expression]) 148 ) 149 ); 150 151 if (t.isIdentifier(expression)) { 152 _hoistedExpressions.set(expression.name, id.name); 153 } 154 155 return id; 156 } 157 ); 158 159 if (variableDeclarators.length) { 160 path 161 .getStatementParent() 162 .insertBefore(t.variableDeclaration('var', variableDeclarators)); 163 } 164 165 return hoistedExpressions.map((id) => { 166 const binding = path.scope.getBinding(id.name); 167 if (binding && t.isVariableDeclarator(binding.path.node)) { 168 const matchPath = binding.path.get('init'); 169 if (this.isMatch(matchPath)) { 170 return { fn: true, id: id.name }; 171 } 172 } 173 174 const input = t.isStringLiteral(id) 175 ? JSON.stringify(id.value) 176 : id.name; 177 return { fn: false, id: input }; 178 }); 179 }, 180 181 _prepareTransform(path) { 182 const transformNode = path.node.tag.arguments[1]; 183 184 if (!transformNode) return null; 185 if (t.isIdentifier(transformNode)) return transformNode.name; 186 187 const matchName = this.getMatchName(path); 188 const id = path.scope.generateUidIdentifier(`${matchName}_transform`); 189 const declarator = t.variableDeclarator(id, transformNode); 190 191 path 192 .getStatementParent() 193 .insertBefore(t.variableDeclaration('var', [declarator])); 194 195 return id.name; 196 }, 197 198 minifyMatch(path) { 199 const quasis = path.node.quasi.quasis.map((x) => 200 t.stringLiteral(x.value.cooked.replace(/\s*/g, '')) 201 ); 202 const expressions = path.node.quasi.expressions; 203 const transform = this._prepareTransform(path); 204 205 path.replaceWith( 206 t.callExpression(path.node.tag, [ 207 t.arrayExpression(quasis), 208 ...expressions, 209 ]) 210 ); 211 }, 212 213 transformMatch(path) { 214 let name = path.node.tag.arguments[0]; 215 if (!name) { 216 name = t.nullLiteral(); 217 } 218 219 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked); 220 221 const expressions = this._prepareExpressions(path); 222 const transform = this._prepareTransform(path); 223 224 let ast; 225 try { 226 ast = parse(quasis, expressions); 227 } catch (error) { 228 if (error.name !== 'SyntaxError') throw error; 229 throw path.get('quasi').buildCodeFrameError(error.message); 230 } 231 232 const code = astRoot(ast, '%%name%%', transform && '%%transform%%'); 233 234 path.replaceWith( 235 template.expression(code)(transform ? { name, transform } : { name }) 236 ); 237 }, 238 }; 239}