diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index b6e369b09f3..c6e007429fb 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -4,33 +4,14 @@ import * as peggy from 'peggy'; const grammar = ` -Start = LogicalOr +Start + = LogicalOr -LogicalOr - = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { - return left || right; - } - / LogicalAnd +WhiteSpace + = " " + / "\t" -LogicalAnd - = left:Primary WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { - return left && right; - } - / Factor - -Factor - = "!" WhiteSpace* operand:Factor { - return !operand; - } - / Primary - -Primary - = Variable - / "(" logicalOr:LogicalOr ")" { - return logicalOr; - } - -Variable +IdentifierPath = variable:Identifier accessor:("." Identifier)* { const path = variable.split( '.' ); let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); @@ -43,11 +24,124 @@ Variable } Identifier - = identifier:$[a-zA-Z0-9_]+ + = !ReservedWord name:IdentifierName { + return name; + } -WhiteSpace - = " " - / "\t" +IdentifierName + = first:IdentifierStart rest:IdentifierPart* { + return first + rest.join( '' ); + } + +IdentifierStart + = [a-zA-Z] + / "_" + / "$" + +IdentifierPart + = IdentifierStart + +ReservedWord + = NullLiteral + / BooleanLiteral + +// Literals + +Literal + = NullLiteral + / BooleanLiteral + / NumericLiteral + +NullLiteral + = NullToken { return null; } + +BooleanLiteral + = "true" { return true; } + / "false" { return false; } + +NumericLiteral + = literal:HexIntegerLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + / literal:DecimalLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + +HexIntegerLiteral + = "0x"i digits:$HexDigit+ { + return parseInt( digits, 16 ); + } + +HexDigit + = [0-9a-f]i + +DecimalLiteral + = DecimalIntegerLiteral "." DecimalDigit* ExponentPart? { + return parseFloat( text() ); + } + / "." DecimalDigit+ ExponentPart? { + return parseFloat( text() ); + } + / DecimalIntegerLiteral ExponentPart? { + return parseFloat( text() ); + } + +DecimalIntegerLiteral + = "0" + / NonZeroDigit DecimalDigit* + +DecimalDigit + = [0-9] + +NonZeroDigit + = [1-9] + +ExponentPart + = ExponentIndicator SignedInteger + +ExponentIndicator + = "e"i + +SignedInteger + = [+-]? DecimalDigit+ + +// Tokens + +NullToken + = "null" !IdentifierPart + +TrueToken + = "true" !IdentifierPart + +FalseToken + = "false" !IdentifierPart + +// Logical Expressions + +LogicalOr + = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { + return left || right; + } + / LogicalAnd + +LogicalAnd + = left:PrimaryExpression WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { + return left && right; + } + / Factor + +Factor + = "!" WhiteSpace* operand:Factor { + return !operand; + } + / PrimaryExpression + +PrimaryExpression + = IdentifierPath + / Literal + / "(" WhiteSpace* expression:LogicalOr WhiteSpace* ")" { + return expression; + } `; export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 5e7627dc041..1ebb246ddc3 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -5,6 +5,54 @@ import { parser } from '../parser'; describe( 'parser', () => { + it( 'should parse a null literal', () => { + const result = parser.parse( 'null' ); + + expect( result ).toEqual( null ); + } ); + + it( 'should parse a boolean true literal', () => { + const result = parser.parse( 'true' ); + + expect( result ).toEqual( true ); + } ); + + it( 'should parse a boolean false literal', () => { + const result = parser.parse( 'false' ); + + expect( result ).toEqual( false ); + } ); + + it( 'should parse a numeric integer literal', () => { + const result = parser.parse( '23' ); + + expect( result ).toEqual( 23 ); + } ); + + it( 'should parse a numeric floating point literal', () => { + const result = parser.parse( '5.23' ); + + expect( result ).toEqual( 5.23 ); + } ); + + it( 'should parse a numeric hexadecimal literal', () => { + const result = parser.parse( '0x23' ); + + expect( result ).toEqual( 35 ); + } ); + + it( 'should parse a string literal with double quotes', () => { + const result = parser.parse( '"foo"' ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should parse a string literal with single quotes', () => { + const result = parser.parse( "'foo'" ); + + expect( result ).toEqual( 'foo' ); + } ); + it( 'should parse a top-level context property', () => { const result = parser.parse( 'foo', { context: { @@ -70,6 +118,6 @@ describe( 'parser', () => { } ); it( 'should throw an error if the expression is invalid', () => { - expect( () => parser.parse( '1' ) ).toThrow(); + expect( () => parser.parse( '= 1' ) ).toThrow(); } ); } );