diff --git a/source/components/JsxParser.test.tsx b/source/components/JsxParser.test.tsx index 14f3458..5402d13 100644 --- a/source/components/JsxParser.test.tsx +++ b/source/components/JsxParser.test.tsx @@ -958,6 +958,24 @@ describe('JsxParser Component', () => { expect(node.childNodes[0].textContent).toEqual(bindings.array[bindings.index].of) expect(instance.ParsedChildren[0].props.foo).toEqual(bindings.array[bindings.index].of) }) + it('can evaluate a[b]', () => { + const { node } = render( + , + ) + expect(node.innerHTML).toMatch('hello') + }) + it('handles optional chaining', () => { + const { node } = render( + , + ) + expect(node.innerHTML).toMatch('baz') + }) /* eslint-enable dot-notation,no-useless-concat */ }) }) @@ -1264,5 +1282,15 @@ describe('JsxParser Component', () => { ) expect(node.outerHTML).toEqual('

from-container

') }) + + it('supports math with scope', () => { + const { node } = render() + expect(node.innerHTML).toEqual('246') + }) + + it('supports conditional with scope', () => { + const { node } = render() + expect(node.innerHTML).toEqual('1-13') + }) }) }) diff --git a/source/components/JsxParser.tsx b/source/components/JsxParser.tsx index 1efe906..9930fc5 100644 --- a/source/components/JsxParser.tsx +++ b/source/components/JsxParser.tsx @@ -94,27 +94,29 @@ export default class JsxParser extends React.Component { case 'ArrayExpression': return expression.elements.map(ele => this.#parseExpression(ele, scope)) as ParsedTree case 'BinaryExpression': + const binaryLeft = this.#parseExpression(expression.left, scope) + const binaryRight = this.#parseExpression(expression.right, scope) /* eslint-disable eqeqeq,max-len */ switch (expression.operator) { - case '-': return this.#parseExpression(expression.left) - this.#parseExpression(expression.right) - case '!=': return this.#parseExpression(expression.left) != this.#parseExpression(expression.right) - case '!==': return this.#parseExpression(expression.left) !== this.#parseExpression(expression.right) - case '*': return this.#parseExpression(expression.left) * this.#parseExpression(expression.right) - case '**': return this.#parseExpression(expression.left) ** this.#parseExpression(expression.right) - case '/': return this.#parseExpression(expression.left) / this.#parseExpression(expression.right) - case '%': return this.#parseExpression(expression.left) % this.#parseExpression(expression.right) - case '+': return this.#parseExpression(expression.left) + this.#parseExpression(expression.right) - case '<': return this.#parseExpression(expression.left) < this.#parseExpression(expression.right) - case '<=': return this.#parseExpression(expression.left) <= this.#parseExpression(expression.right) - case '==': return this.#parseExpression(expression.left) == this.#parseExpression(expression.right) - case '===': return this.#parseExpression(expression.left) === this.#parseExpression(expression.right) - case '>': return this.#parseExpression(expression.left) > this.#parseExpression(expression.right) - case '>=': return this.#parseExpression(expression.left) >= this.#parseExpression(expression.right) + case '-': return binaryLeft - binaryRight + case '!=': return binaryLeft != binaryRight + case '!==': return binaryLeft !== binaryRight + case '*': return binaryLeft * binaryRight + case '**': return binaryLeft ** binaryRight + case '/': return binaryLeft / binaryRight + case '%': return binaryLeft % binaryRight + case '+': return binaryLeft + binaryRight + case '<': return binaryLeft < binaryRight + case '<=': return binaryLeft <= binaryRight + case '==': return binaryLeft == binaryRight + case '===': return binaryLeft === binaryRight + case '>': return binaryLeft > binaryRight + case '>=': return binaryLeft >= binaryRight /* eslint-enable eqeqeq,max-len */ } return undefined case 'CallExpression': - const parsedCallee = this.#parseExpression(expression.callee) + const parsedCallee = this.#parseExpression(expression.callee, scope) if (parsedCallee === undefined) { this.props.onError!(new Error(`The expression '${expression.callee}' could not be resolved, resulting in an undefined return value.`)) return undefined @@ -123,11 +125,11 @@ export default class JsxParser extends React.Component { arg => this.#parseExpression(arg, expression.callee), )) case 'ConditionalExpression': - return this.#parseExpression(expression.test) - ? this.#parseExpression(expression.consequent) - : this.#parseExpression(expression.alternate) + return this.#parseExpression(expression.test, scope) + ? this.#parseExpression(expression.consequent, scope) + : this.#parseExpression(expression.alternate, scope) case 'ExpressionStatement': - return this.#parseExpression(expression.expression) + return this.#parseExpression(expression.expression, scope) case 'Identifier': if (scope && expression.name in scope) { return scope[expression.name] @@ -137,10 +139,10 @@ export default class JsxParser extends React.Component { case 'Literal': return expression.value case 'LogicalExpression': - const left = this.#parseExpression(expression.left) + const left = this.#parseExpression(expression.left, scope) if (expression.operator === '||' && left) return left if ((expression.operator === '&&' && left) || (expression.operator === '||' && !left)) { - return this.#parseExpression(expression.right) + return this.#parseExpression(expression.right, scope) } return false case 'MemberExpression': @@ -148,7 +150,7 @@ export default class JsxParser extends React.Component { case 'ObjectExpression': const object: Record = {} expression.properties.forEach(prop => { - object[prop.key.name! || prop.key.value!] = this.#parseExpression(prop.value) + object[prop.key.name! || prop.key.value!] = this.#parseExpression(prop.value, scope) }) return object case 'TemplateElement': @@ -159,7 +161,7 @@ export default class JsxParser extends React.Component { if (a.start < b.start) return -1 return 1 }) - .map(item => this.#parseExpression(item)) + .map(item => this.#parseExpression(item, scope)) .join('') case 'UnaryExpression': switch (expression.operator) { @@ -183,37 +185,25 @@ export default class JsxParser extends React.Component { } #parseMemberExpression = (expression: AcornJSX.MemberExpression, scope?: Scope): any => { - // eslint-disable-next-line prefer-destructuring - let { object } = expression - const path = [expression.property?.name ?? JSON.parse(expression.property?.raw ?? '""')] - - if (expression.object.type !== 'Literal') { - while (object && ['MemberExpression', 'Literal'].includes(object?.type)) { - const { property } = (object as AcornJSX.MemberExpression) - if ((object as AcornJSX.MemberExpression).computed) { - path.unshift(this.#parseExpression(property!, scope)) - } else { - path.unshift(property?.name ?? JSON.parse(property?.raw ?? '""')) - } + const object = this.#parseExpression(expression.object, scope) - object = (object as AcornJSX.MemberExpression).object - } + let property + if (expression.computed) { + property = this.#parseExpression(expression.property, scope) + } else if (expression.property.type === 'Identifier') { + property = expression.property.name + } else { + this.props.onError!(new Error('Only simple MemberExpressions are supported.')) + return undefined } - const target = this.#parseExpression(object, scope) - try { - let parent = target - const member = path.reduce((value, next) => { - parent = value - return value[next] - }, target) - if (typeof member === 'function') return member.bind(parent) - - return member - } catch { - const name = (object as AcornJSX.MemberExpression)?.name || 'unknown' - this.props.onError!(new Error(`Unable to parse ${name}["${path.join('"]["')}"]}`)) + if (expression.optional) { + if (object === null || object === undefined) return undefined } + + const member = object[property] + if (typeof member === 'function') return member.bind(object) + return member } #parseName = (element: AcornJSX.JSXIdentifier | AcornJSX.JSXMemberExpression): string => { diff --git a/source/types/acorn-jsx.d.ts b/source/types/acorn-jsx.d.ts index 5af688c..2477f57 100644 --- a/source/types/acorn-jsx.d.ts +++ b/source/types/acorn-jsx.d.ts @@ -122,9 +122,10 @@ declare module 'acorn-jsx' { export interface MemberExpression extends BaseExpression { type: 'MemberExpression'; computed: boolean; + optional: boolean; name?: string; - object: Literal | MemberExpression; - property?: MemberExpression; + object: Expression; + property: Expression; raw?: string; }