diff --git a/.changeset/moody-papayas-battle.md b/.changeset/moody-papayas-battle.md new file mode 100644 index 00000000..e4ced694 --- /dev/null +++ b/.changeset/moody-papayas-battle.md @@ -0,0 +1,5 @@ +--- +'@rekajs/parser': patch +--- + +Allow ability to normalise nodes on parse diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 25cc0544..07a91bba 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -5,10 +5,12 @@ import acorn, { Parser as AcornParser } from 'acorn'; import jsx from 'acorn-jsx'; import { Lexer } from './lexer'; -import { Stringifier } from './stringifier'; +import { Stringifier, StringifierOpts } from './stringifier'; import { TokenType } from './tokens'; import { getIdentifierFromStr } from './utils'; +export { StringifierOpts } from './stringifier'; + const parseWithAcorn = (source: string, loc: number) => { const JSXParser = AcornParser.extend(jsx()); @@ -17,7 +19,7 @@ const parseWithAcorn = (source: string, loc: number) => { }) as b.Node & acorn.Node; }; -type AcornParserOptions = { +type AcornParserOptions = ParserOpts & { expectedType?: t.TypeConstructor; isElementEachDirective?: boolean; }; @@ -84,247 +86,264 @@ const jsToReka = ( opts?: AcornParserOptions ) => { const _convert = (node: b.Node) => { - switch (node.type) { - case 'BlockStatement': { - return t.block({ - statements: node.body.map((b) => _convert(b)), - }); - } - case 'AssignmentExpression': { - return t.assignment({ - left: _convert(node.left), - operator: node.operator as any, - right: _convert(node.right), - }); - } - case 'VariableDeclaration': { - return t.val({ - name: (node.declarations[0].id as b.Identifier).name, - init: node.declarations[0].init - ? _convert(node.declarations[0].init) - : undefined, - }); - } - case 'Identifier': { - return getIdentifierFromStr(node.name); - } - case 'ExpressionStatement': { - return _convert(node.expression); - } - case 'ArrowFunctionExpression': { - return t.func({ - params: node.params.map((p) => { - b.assertIdentifier(p); - - return t.param({ - name: p.name, - }); - }), - body: _convert(node.body as b.BlockStatement), - }); - } - case 'ArrayExpression': { - return t.arrayExpression({ - elements: node.elements.map((p) => p && _convert(p)), - }); - } - case 'ObjectExpression': { - return t.objectExpression({ - properties: node.properties.reduce((accum, property: any) => { - let key: string; + const _convertAcornNode = () => { + switch (node.type) { + case 'BlockStatement': { + return t.block({ + statements: node.body.map((b) => _convert(b)), + }); + } + case 'AssignmentExpression': { + return t.assignment({ + left: _convert(node.left), + operator: node.operator as any, + right: _convert(node.right), + }); + } + case 'VariableDeclaration': { + return t.val({ + name: (node.declarations[0].id as b.Identifier).name, + init: node.declarations[0].init + ? _convert(node.declarations[0].init) + : undefined, + }); + } + case 'Identifier': { + return getIdentifierFromStr(node.name); + } + case 'ExpressionStatement': { + return _convert(node.expression); + } + case 'ArrowFunctionExpression': { + return t.func({ + params: node.params.map((p) => { + b.assertIdentifier(p); + + return t.param({ + name: p.name, + }); + }), + body: _convert(node.body as b.BlockStatement), + }); + } + case 'ArrayExpression': { + return t.arrayExpression({ + elements: node.elements.map((p) => p && _convert(p)), + }); + } + case 'ObjectExpression': { + return t.objectExpression({ + properties: node.properties.reduce((accum, property: any) => { + let key: string; + + if (property.key.type === 'Literal') { + key = property.key.value; + } else { + key = property.key.name; + } + + return { + ...accum, + [`${safeObjKey(key)}`]: _convert(property.value), + }; + }, {}), + }); + } + case 'CallExpression': { + const identifier = _convert(node.callee) as t.Identifier; - if (property.key.type === 'Literal') { - key = property.key.value; + return t.callExpression({ + identifier, + arguments: node.arguments.map((arg) => _convert(arg)), + }); + } + case 'IfStatement': { + return t.ifStatement({ + condition: _convert(node.test), + consequent: _convert(node.consequent), + }); + } + case 'ConditionalExpression': { + return t.conditionalExpression({ + condition: _convert(node.test), + consequent: _convert(node.consequent), + alternate: _convert(node.alternate), + }); + } + case 'BinaryExpression': { + if (node.operator === 'in' && opts?.isElementEachDirective) { + let alias: t.Identifier; + let index: t.Identifier | undefined; + + if (b.isIdentifier(node.left)) { + alias = _convert(node.left); + } else if (b.isSequenceExpression(node.left)) { + b.assertIdentifier(node.left.expressions[0]); + b.assertIdentifier(node.left.expressions[1]); + + alias = _convert(node.left.expressions[0]); + index = _convert(node.left.expressions[1]); } else { - key = property.key.name; + throw new Error( + 'Unexpected left hand side input for constructing ElementEach type' + ); } - return { - ...accum, - [`${safeObjKey(key)}`]: _convert(property.value), - }; - }, {}), - }); - } - case 'CallExpression': { - const identifier = _convert(node.callee) as t.Identifier; - - return t.callExpression({ - identifier, - arguments: node.arguments.map((arg) => _convert(arg)), - }); - } - case 'IfStatement': { - return t.ifStatement({ - condition: _convert(node.test), - consequent: _convert(node.consequent), - }); - } - case 'ConditionalExpression': { - return t.conditionalExpression({ - condition: _convert(node.test), - consequent: _convert(node.consequent), - alternate: _convert(node.alternate), - }); - } - case 'BinaryExpression': { - if (node.operator === 'in' && opts?.isElementEachDirective) { - let alias: t.Identifier; - let index: t.Identifier | undefined; - - if (b.isIdentifier(node.left)) { - alias = _convert(node.left); - } else if (b.isSequenceExpression(node.left)) { - b.assertIdentifier(node.left.expressions[0]); - b.assertIdentifier(node.left.expressions[1]); - - alias = _convert(node.left.expressions[0]); - index = _convert(node.left.expressions[1]); - } else { - throw new Error( - 'Unexpected left hand side input for constructing ElementEach type' - ); + return t.elementEach({ + alias: t.elementEachAlias({ + name: alias.name, + }), + index: index + ? t.elementEachIndex({ + name: index.name, + }) + : undefined, + iterator: _convert(node.right), + }); } - return t.elementEach({ - alias: t.elementEachAlias({ - name: alias.name, - }), - index: index - ? t.elementEachIndex({ - name: index.name, - }) - : undefined, - iterator: _convert(node.right), + return t.binaryExpression({ + left: _convert(node.left), + operator: node.operator as any, + right: _convert(node.right), }); } + case 'JSXElement': { + const identifier = node.openingElement.name; + invariant(b.isJSXIdentifier(identifier), 'Invalid JSX identifier'); - return t.binaryExpression({ - left: _convert(node.left), - operator: node.operator as any, - right: _convert(node.right), - }); - } - case 'JSXElement': { - const identifier = node.openingElement.name; - invariant(b.isJSXIdentifier(identifier), 'Invalid JSX identifier'); + const identifierName = identifier.name; - const identifierName = identifier.name; + const isComponent = + identifierName[0] === identifierName[0].toUpperCase(); - const isComponent = - identifierName[0] === identifierName[0].toUpperCase(); + const directives = { + if: null, + each: null, + classList: null, + }; - const directives = { - if: null, - each: null, - classList: null, - }; + const props = node.openingElement.attributes.reduce((accum, attr) => { + invariant(b.isJSXAttribute(attr), 'Invalid attribute'); - const props = node.openingElement.attributes.reduce((accum, attr) => { - invariant(b.isJSXAttribute(attr), 'Invalid attribute'); + const attrName = attr.name.name; + invariant(typeof attrName === 'string', 'Invalid attribute name'); - const attrName = attr.name.name; - invariant(typeof attrName === 'string', 'Invalid attribute name'); + if ( + attrName.startsWith('@') && + Object.keys(directives).includes(attrName.substring(1)) + ) { + directives[attrName.substring(1)] = attr.value + ? _convert(attr.value) + : null; - if ( - attrName.startsWith('@') && - Object.keys(directives).includes(attrName.substring(1)) - ) { - directives[attrName.substring(1)] = attr.value - ? _convert(attr.value) - : null; + return accum; + } - return accum; + return { + ...accum, + [attrName]: attr.value ? _convert(attr.value) : undefined, + }; + }, {}); + + const children = node.children.map((child) => _convert(child)); + + if (isComponent) { + return t.componentTemplate({ + component: t.identifier({ + name: identifierName, + }), + props, + children, + ...directives, + }); } - return { - ...accum, - [attrName]: attr.value ? _convert(attr.value) : undefined, - }; - }, {}); - - const children = node.children.map((child) => _convert(child)); + if (identifierName === 'slot') { + return t.slotTemplate({ + props: {}, + }); + } - if (isComponent) { - return t.componentTemplate({ - component: t.identifier({ - name: identifierName, - }), + return t.tagTemplate({ + tag: identifierName, props, children, ...directives, }); } - - if (identifierName === 'slot') { - return t.slotTemplate({ - props: {}, + case 'JSXExpressionContainer': { + return t.Schema.fromJSON(node.expression); + } + case 'MemberExpression': { + return convertMemberExpression(node); + } + case 'LogicalExpression': { + return t.binaryExpression({ + left: _convert(node.left), + operator: node.operator, + right: _convert(node.right), }); } + case 'TemplateLiteral': { + const str = node.quasis.map((quasi) => quasi.value.raw).join(''); - return t.tagTemplate({ - tag: identifierName, - props, - children, - ...directives, - }); - } - case 'JSXExpressionContainer': { - return t.Schema.fromJSON(node.expression); - } - case 'MemberExpression': { - return convertMemberExpression(node); - } - case 'LogicalExpression': { - return t.binaryExpression({ - left: _convert(node.left), - operator: node.operator, - right: _convert(node.right), - }); - } - case 'TemplateLiteral': { - const str = node.quasis.map((quasi) => quasi.value.raw).join(''); + const bracesMatches = [...str.matchAll(/{{(.*?)}}/g)]; - const bracesMatches = [...str.matchAll(/{{(.*?)}}/g)]; + if (bracesMatches.length == 0) { + return t.string({ + value: [str], + }); + } - if (bracesMatches.length == 0) { return t.string({ - value: [str], + value: bracesMatches.reduce((accum, match, matchIdx) => { + const exprStr = match[0]; + const { type: expr } = parseExpressionWithAcornToRekaType( + exprStr.substring(2, exprStr.length - 2), + 0, + opts + ); + + const start = + matchIdx === 0 + ? 0 + : (bracesMatches[matchIdx - 1].index ?? 0) + + bracesMatches[matchIdx - 1][0].length; + + const innerStr = str.substring(start, match.index); + accum.push(innerStr); + accum.push(expr); + + if (matchIdx === bracesMatches.length - 1) { + accum.push(str.substring(match.index! + exprStr.length)); + } + return accum; + }, [] as Array), }); } + default: { + return t.Schema.fromJSON(node) as t.Type; + } + } + }; - return t.string({ - value: bracesMatches.reduce((accum, match, matchIdx) => { - const exprStr = match[0]; - const { type: expr } = parseExpressionWithAcornToRekaType( - exprStr.substring(2, exprStr.length - 2), - 0 - ); - - const start = - matchIdx === 0 - ? 0 - : (bracesMatches[matchIdx - 1].index ?? 0) + - bracesMatches[matchIdx - 1][0].length; + let convertedNode = _convertAcornNode(); - const innerStr = str.substring(start, match.index); - accum.push(innerStr); - accum.push(expr); + if (opts?.onParseNode) { + const newType = opts?.onParseNode(convertedNode); - if (matchIdx === bracesMatches.length - 1) { - accum.push(str.substring(match.index! + exprStr.length)); - } - return accum; - }, [] as Array), - }); - } - default: { - return t.Schema.fromJSON(node) as t.Type; + if (newType) { + convertedNode = newType; } + + return convertedNode; } + + return convertedNode; }; - const type = _convert(node) as t.Any; + let type = _convert(node) as t.ASTNode; if (opts?.expectedType) { invariant( @@ -336,7 +355,19 @@ const jsToReka = ( return type as T; }; +export type onParseNode = ( + node: t.ASTNode +) => t.ASTNode | undefined | null | void; + +export type ParserOpts = { + onParseNode?: onParseNode; +}; + class _Parser extends Lexer { + constructor(source: string, readonly opts?: ParserOpts) { + super(source); + } + parse() { this.next(); @@ -518,7 +549,11 @@ class _Parser extends Lexer { const exprString = `(${this.source.slice(startTokenPos, endTokenPos)})`; - init = parseExpressionWithAcornToRekaType(exprString, 0).type; + init = parseExpressionWithAcornToRekaType( + exprString, + 0, + this.opts + ).type; } props.push( @@ -717,7 +752,10 @@ class _Parser extends Lexer { const { expression, type } = parseExpressionWithAcornToRekaType( this.source, loc, - opts + { + onParseNode: this.opts?.onParseNode, + ...opts, + } ); // Since we're using acorn to parse the expression @@ -734,24 +772,25 @@ class _Parser extends Lexer { */ export class Parser { /// Parse source into a Reka Program AST node - static parseProgram(source: string) { - return new _Parser(source).parse(); + static parseProgram(source: string, opts?: ParserOpts) { + return new _Parser(source, opts).parse(); } /// Parse an expression string into a Expression AST node static parseExpression( source: string, - expectedType?: t.TypeConstructor + opts?: Partial }> ) { const { type } = parseExpressionWithAcornToRekaType(`{${source}}`, 1, { - expectedType, + onParseNode: opts?.onParseNode, + expectedType: opts?.expected, }); return type as T; } /// Stringify an AST Node into code - static stringify(type: t.ASTNode) { - return Stringifier.toString(type); + static stringify(type: t.ASTNode, opts?: StringifierOpts) { + return Stringifier.toString(type, opts); } } diff --git a/packages/parser/src/stringifier.ts b/packages/parser/src/stringifier.ts index d9a72d84..1c74af88 100644 --- a/packages/parser/src/stringifier.ts +++ b/packages/parser/src/stringifier.ts @@ -5,8 +5,25 @@ import { BinaryPrecedence, Precedence } from './precedence'; import { EXTERNAL_IDENTIFIER_PREFIX_SYMBOL } from './utils'; import { Writer, WriterResult } from './writer'; +export type StringifierOpts = { + onStringifyNode: ( + node: t.ASTNode, + stringifier: _Stringifier + ) => t.ASTNode | null | undefined; +}; + class _Stringifier { writer: Writer = new Writer(); + opts: StringifierOpts; + + constructor(opts?: StringifierOpts) { + this.opts = { + onStringifyNode: () => { + return null; + }, + ...opts, + }; + } private stringifyInput(input: t.Kind) { const _stringifyInputType = (input: t.Kind) => { @@ -84,6 +101,12 @@ class _Stringifier { } stringify(node: t.ASTNode, precedence: Precedence = Precedence.Sequence) { + const value = this.opts.onStringifyNode(node, this); + + if (value) { + node = value; + } + return t.match(node, { Literal: (node) => { if (typeof node.value === 'string') { @@ -499,8 +522,8 @@ class _Stringifier { } export class Stringifier { - static toString(node: t.ASTNode) { - const _stringifer = new _Stringifier(); + static toString(node: t.ASTNode, opts?: StringifierOpts) { + const _stringifer = new _Stringifier(opts); return _stringifer.toString(node); } }