@@ -29,6 +29,13 @@ export type TProps = {
2929}
3030type Scope = Record < string , any >
3131
32+ class NullishShortCircuit extends Error {
33+ constructor ( message = 'Nullish value encountered' ) {
34+ super ( message )
35+ this . name = 'NullishShortCircuit'
36+ }
37+ }
38+
3239/* eslint-disable consistent-return */
3340export default class JsxParser extends React . Component < TProps > {
3441 static displayName = 'JsxParser'
@@ -94,40 +101,45 @@ export default class JsxParser extends React.Component<TProps> {
94101 case 'ArrayExpression' :
95102 return expression . elements . map ( ele => this . #parseExpression( ele , scope ) ) as ParsedTree
96103 case 'BinaryExpression' :
104+ const binaryLeft = this . #parseExpression( expression . left , scope )
105+ const binaryRight = this . #parseExpression( expression . right , scope )
97106 /* eslint-disable eqeqeq,max-len */
98107 switch ( expression . operator ) {
99- case '-' : return this . #parseExpression ( expression . left ) - this . #parseExpression ( expression . right )
100- case '!=' : return this . #parseExpression ( expression . left ) != this . #parseExpression ( expression . right )
101- case '!==' : return this . #parseExpression ( expression . left ) !== this . #parseExpression ( expression . right )
102- case '*' : return this . #parseExpression ( expression . left ) * this . #parseExpression ( expression . right )
103- case '**' : return this . #parseExpression ( expression . left ) ** this . #parseExpression ( expression . right )
104- case '/' : return this . #parseExpression ( expression . left ) / this . #parseExpression ( expression . right )
105- case '%' : return this . #parseExpression ( expression . left ) % this . #parseExpression ( expression . right )
106- case '+' : return this . #parseExpression ( expression . left ) + this . #parseExpression ( expression . right )
107- case '<' : return this . #parseExpression ( expression . left ) < this . #parseExpression ( expression . right )
108- case '<=' : return this . #parseExpression ( expression . left ) <= this . #parseExpression ( expression . right )
109- case '==' : return this . #parseExpression ( expression . left ) == this . #parseExpression ( expression . right )
110- case '===' : return this . #parseExpression ( expression . left ) === this . #parseExpression ( expression . right )
111- case '>' : return this . #parseExpression ( expression . left ) > this . #parseExpression ( expression . right )
112- case '>=' : return this . #parseExpression ( expression . left ) >= this . #parseExpression ( expression . right )
108+ case '-' : return binaryLeft - binaryRight
109+ case '!=' : return binaryLeft != binaryRight
110+ case '!==' : return binaryLeft !== binaryRight
111+ case '*' : return binaryLeft * binaryRight
112+ case '**' : return binaryLeft ** binaryRight
113+ case '/' : return binaryLeft / binaryRight
114+ case '%' : return binaryLeft % binaryRight
115+ case '+' : return binaryLeft + binaryRight
116+ case '<' : return binaryLeft < binaryRight
117+ case '<=' : return binaryLeft <= binaryRight
118+ case '==' : return binaryLeft == binaryRight
119+ case '===' : return binaryLeft === binaryRight
120+ case '>' : return binaryLeft > binaryRight
121+ case '>=' : return binaryLeft >= binaryRight
113122 /* eslint-enable eqeqeq,max-len */
114123 }
115124 return undefined
116125 case 'CallExpression' :
117- const parsedCallee = this . #parseExpression( expression . callee )
126+ const parsedCallee = this . #parseExpression( expression . callee , scope )
118127 if ( parsedCallee === undefined ) {
128+ if ( expression . optional ) {
129+ throw new NullishShortCircuit ( )
130+ }
119131 this . props . onError ! ( new Error ( `The expression '${ expression . callee } ' could not be resolved, resulting in an undefined return value.` ) )
120132 return undefined
121133 }
122134 return parsedCallee ( ...expression . arguments . map (
123135 arg => this . #parseExpression( arg , expression . callee ) ,
124136 ) )
125137 case 'ConditionalExpression' :
126- return this . #parseExpression( expression . test )
127- ? this . #parseExpression( expression . consequent )
128- : this . #parseExpression( expression . alternate )
138+ return this . #parseExpression( expression . test , scope )
139+ ? this . #parseExpression( expression . consequent , scope )
140+ : this . #parseExpression( expression . alternate , scope )
129141 case 'ExpressionStatement' :
130- return this . #parseExpression( expression . expression )
142+ return this . #parseExpression( expression . expression , scope )
131143 case 'Identifier' :
132144 if ( scope && expression . name in scope ) {
133145 return scope [ expression . name ]
@@ -137,18 +149,20 @@ export default class JsxParser extends React.Component<TProps> {
137149 case 'Literal' :
138150 return expression . value
139151 case 'LogicalExpression' :
140- const left = this . #parseExpression( expression . left )
152+ const left = this . #parseExpression( expression . left , scope )
141153 if ( expression . operator === '||' && left ) return left
142154 if ( ( expression . operator === '&&' && left ) || ( expression . operator === '||' && ! left ) ) {
143- return this . #parseExpression( expression . right )
155+ return this . #parseExpression( expression . right , scope )
144156 }
145157 return false
146158 case 'MemberExpression' :
147159 return this . #parseMemberExpression( expression , scope )
160+ case 'ChainExpression' :
161+ return this . #parseChainExpression( expression , scope )
148162 case 'ObjectExpression' :
149163 const object : Record < string , any > = { }
150164 expression . properties . forEach ( prop => {
151- object [ prop . key . name ! || prop . key . value ! ] = this . #parseExpression( prop . value )
165+ object [ prop . key . name ! || prop . key . value ! ] = this . #parseExpression( prop . value , scope )
152166 } )
153167 return object
154168 case 'TemplateElement' :
@@ -159,7 +173,7 @@ export default class JsxParser extends React.Component<TProps> {
159173 if ( a . start < b . start ) return - 1
160174 return 1
161175 } )
162- . map ( item => this . #parseExpression( item ) )
176+ . map ( item => this . #parseExpression( item , scope ) )
163177 . join ( '' )
164178 case 'UnaryExpression' :
165179 switch ( expression . operator ) {
@@ -179,41 +193,48 @@ export default class JsxParser extends React.Component<TProps> {
179193 } )
180194 return this . #parseExpression( expression . body , functionScope )
181195 }
196+ default :
197+ this . props . onError ! ( new Error ( `The expression type '${ expression . type } ' is not supported.` ) )
198+ return undefined
199+ }
200+ }
201+
202+ #parseChainExpression = ( expression : AcornJSX . ChainExpression , scope ?: Scope ) : any => {
203+ try {
204+ return this . #parseExpression( expression . expression , scope )
205+ } catch ( error ) {
206+ if ( error instanceof NullishShortCircuit ) return undefined
207+ throw error
182208 }
183209 }
184210
185211 #parseMemberExpression = ( expression : AcornJSX . MemberExpression , scope ?: Scope ) : any => {
186- // eslint-disable-next-line prefer-destructuring
187- let { object } = expression
188- const path = [ expression . property ?. name ?? JSON . parse ( expression . property ?. raw ?? '""' ) ]
212+ const object = this . #parseExpression( expression . object , scope )
189213
190- if ( expression . object . type !== 'Literal' ) {
191- while ( object && [ 'MemberExpression' , 'Literal' ] . includes ( object ?. type ) ) {
192- const { property } = ( object as AcornJSX . MemberExpression )
193- if ( ( object as AcornJSX . MemberExpression ) . computed ) {
194- path . unshift ( this . #parseExpression( property ! , scope ) )
195- } else {
196- path . unshift ( property ?. name ?? JSON . parse ( property ?. raw ?? '""' ) )
197- }
214+ let property
198215
199- object = ( object as AcornJSX . MemberExpression ) . object
200- }
216+ if ( expression . computed ) {
217+ property = this . #parseExpression( expression . property , scope )
218+ } else if ( expression . property . type === 'Identifier' ) {
219+ property = expression . property . name
220+ } else {
221+ this . props . onError ! ( new Error ( 'Only simple MemberExpressions are supported.' ) )
222+ return undefined
201223 }
202224
203- const target = this . #parseExpression( object , scope )
204- try {
205- let parent = target
206- const member = path . reduce ( ( value , next ) => {
207- parent = value
208- return value [ next ]
209- } , target )
210- if ( typeof member === 'function' ) return member . bind ( parent )
225+ if ( object === null || object === undefined ) {
226+ if ( expression . optional ) throw new NullishShortCircuit ( )
227+ }
211228
212- return member
213- } catch {
214- const name = ( object as AcornJSX . MemberExpression ) ?. name || 'unknown'
215- this . props . onError ! ( new Error ( `Unable to parse ${ name } ["${ path . join ( '"]["' ) } "]}` ) )
229+ let member
230+ try {
231+ member = object [ property ]
232+ } catch ( error ) {
233+ this . props . onError ! ( new Error ( `The property '${ property } ' could not be resolved on the object '${ object } '.` ) )
234+ return undefined
216235 }
236+ if ( typeof member === 'function' ) return member . bind ( object )
237+ return member
217238 }
218239
219240 #parseName = ( element : AcornJSX . JSXIdentifier | AcornJSX . JSXMemberExpression ) : string => {
0 commit comments