Mirror: The magical sticky regex-based parser generator 馃
at v3.0.0 7.9 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 const { confident, value } = nameArgumentPath.evaluate(); 87 if (!confident && t.isIdentifier(nameArgumentPath.node)) { 88 return nameArgumentPath.node.name; 89 } else if (confident && typeof value === 'string') { 90 return value; 91 } else { 92 return path.scope.generateUidIdentifierBasedOnNode(path.node); 93 } 94 }, 95 96 /** Given a match, hoists its expressions in front of the match's statement */ 97 _prepareExpressions(path) { 98 t.assertTaggedTemplateExpression(path.node); 99 100 const variableDeclarators = []; 101 const matchName = this.getMatchName(path); 102 103 const hoistedExpressions = path.node.quasi.expressions.map( 104 (expression, i) => { 105 if ( 106 t.isArrowFunctionExpression(expression) && 107 t.isIdentifier(expression.body) 108 ) { 109 expression = expression.body; 110 } else if ( 111 (t.isFunctionExpression(expression) || 112 t.isArrowFunctionExpression(expression)) && 113 t.isBlockStatement(expression.body) && 114 expression.body.body.length === 1 && 115 t.isReturnStatement(expression.body.body[0]) && 116 t.isIdentifier(expression.body.body[0].argument) 117 ) { 118 expression = expression.body.body[0].argument; 119 } 120 121 const isBindingExpression = 122 t.isIdentifier(expression) && 123 path.scope.hasBinding(expression.name); 124 if (isBindingExpression) { 125 const binding = path.scope.getBinding(expression.name); 126 if (t.isVariableDeclarator(binding.path.node)) { 127 const matchPath = binding.path.get('init'); 128 if (this.isMatch(matchPath)) { 129 return expression; 130 } else if (_hoistedExpressions.has(expression.name)) { 131 return t.identifier(_hoistedExpressions.get(expression.name)); 132 } 133 } 134 } 135 136 const id = path.scope.generateUidIdentifier( 137 isBindingExpression 138 ? `${expression.name}_expression` 139 : `${matchName}_expression` 140 ); 141 142 variableDeclarators.push( 143 t.variableDeclarator( 144 id, 145 t.callExpression(t.identifier(_patternId.name), [expression]) 146 ) 147 ); 148 149 if (t.isIdentifier(expression)) { 150 _hoistedExpressions.set(expression.name, id.name); 151 } 152 153 return id; 154 } 155 ); 156 157 if (variableDeclarators.length) { 158 path 159 .getStatementParent() 160 .insertBefore(t.variableDeclaration('var', variableDeclarators)); 161 } 162 163 return hoistedExpressions.map((id) => { 164 const binding = path.scope.getBinding(id.name); 165 if (binding && t.isVariableDeclarator(binding.path.node)) { 166 const matchPath = binding.path.get('init'); 167 if (this.isMatch(matchPath)) { 168 return { fn: true, id: id.name }; 169 } 170 } 171 172 const input = t.isStringLiteral(id) 173 ? JSON.stringify(id.value) 174 : id.name; 175 return { fn: false, id: input }; 176 }); 177 }, 178 179 _prepareTransform(path) { 180 const transformNode = path.node.tag.arguments[1]; 181 182 if (!transformNode) return null; 183 if (t.isIdentifier(transformNode)) return transformNode.name; 184 185 const matchName = this.getMatchName(path); 186 const id = path.scope.generateUidIdentifier(`${matchName}_transform`); 187 const declarator = t.variableDeclarator(id, transformNode); 188 189 path 190 .getStatementParent() 191 .insertBefore(t.variableDeclaration('var', [declarator])); 192 193 return id.name; 194 }, 195 196 minifyMatch(path) { 197 if (!path.node.tag.arguments.length) { 198 throw path 199 .get('tag') 200 .buildCodeFrameError( 201 'match() must at least be called with a node name' 202 ); 203 } 204 205 const quasis = path.node.quasi.quasis.map((x) => 206 t.stringLiteral(x.value.cooked.replace(/\s*/g, '')) 207 ); 208 const expressions = path.node.quasi.expressions; 209 const transform = this._prepareTransform(path); 210 211 path.replaceWith( 212 t.callExpression(path.node.tag, [ 213 t.arrayExpression(quasis), 214 ...expressions, 215 ]) 216 ); 217 }, 218 219 transformMatch(path) { 220 if (!path.node.tag.arguments.length) { 221 throw path 222 .get('tag') 223 .buildCodeFrameError( 224 'match() must at least be called with a node name' 225 ); 226 } 227 228 const name = path.node.tag.arguments[0]; 229 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked); 230 231 const expressions = this._prepareExpressions(path); 232 const transform = this._prepareTransform(path); 233 234 let ast; 235 try { 236 ast = parse(quasis, expressions); 237 } catch (error) { 238 if (error.name !== 'SyntaxError') throw error; 239 throw path.get('quasi').buildCodeFrameError(error.message); 240 } 241 242 const code = astRoot(ast, '%%name%%', transform && '%%transform%%'); 243 244 path.replaceWith( 245 template.expression(code)(transform ? { name, transform } : { name }) 246 ); 247 }, 248 }; 249}