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