Mirror: The magical sticky regex-based parser generator 馃
at v2.0.3 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)) { 162 return { fn: true, id: id.name }; 163 } 164 } 165 166 const input = t.isStringLiteral(id) 167 ? JSON.stringify(id.value) 168 : id.name; 169 return { fn: false, id: input }; 170 }); 171 }, 172 173 _prepareTransform(path) { 174 const transformNode = path.node.tag.arguments[1]; 175 176 if (!transformNode) return null; 177 if (t.isIdentifier(transformNode)) return transformNode.name; 178 179 const matchName = this.getMatchName(path); 180 const id = path.scope.generateUidIdentifier(`${matchName}_transform`); 181 const declarator = t.variableDeclarator(id, transformNode); 182 183 path 184 .getStatementParent() 185 .insertBefore(t.variableDeclaration('var', [declarator])); 186 187 return id.name; 188 }, 189 190 minifyMatch(path) { 191 if (!path.node.tag.arguments.length) { 192 throw path 193 .get('tag') 194 .buildCodeFrameError( 195 'match() must at least be called with a node name' 196 ); 197 } 198 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 if (!path.node.tag.arguments.length) { 215 throw path 216 .get('tag') 217 .buildCodeFrameError( 218 'match() must at least be called with a node name' 219 ); 220 } 221 222 const name = path.node.tag.arguments[0]; 223 const quasis = path.node.quasi.quasis.map((x) => x.value.cooked); 224 225 const expressions = this._prepareExpressions(path); 226 const transform = this._prepareTransform(path); 227 228 let ast; 229 try { 230 ast = parse(quasis, expressions); 231 } catch (error) { 232 if (error.name !== 'SyntaxError') throw error; 233 throw path.get('quasi').buildCodeFrameError(error.message); 234 } 235 236 const code = astRoot(ast, '%%name%%', transform && '%%transform%%'); 237 238 path.replaceWith( 239 template.expression(code)(transform ? { name, transform } : { name }) 240 ); 241 }, 242 }; 243}