Simple PEG.JS grammer to parse logical expressions like:
test in(1, "2") and (flag = true or test ~ "test" or price >= 100.00 and (total > 100 or total < 200))
Which will output an AST that looks like:
{
"type": "expression",
"left": {
"type": "functionTerm",
"variable": "test",
"function": "in",
"args": [
1,
"2"
]
},
"operator": "and",
"right": {
"type": "group",
"body": {
"type": "expression",
"left": {
"type": "term",
"variable": "flag",
"operator": "=",
"value": true
},
"operator": "or",
"right": {
"type": "expression",
"left": {
"type": "term",
"variable": "test",
"operator": "contains",
"value": "test"
},
"operator": "or",
"right": {
"type": "expression",
"left": {
"type": "term",
"variable": "price",
"operator": ">=",
"value": 100
},
"operator": "and",
"right": {
"type": "group",
"body": {
"type": "expression",
"left": {
"type": "term",
"variable": "total",
"operator": ">",
"value": 100
},
"operator": "or",
"right": {
"type": "term",
"variable": "total",
"operator": "<",
"value": 200
}
}
}
}
}
}
}
}
It also supports escaping "
characters using \
.
start
= Expression
Expression
= left:(Term / FunctionTerm / GroupedExpression) Whitespace op:LogicalOperator Whitespace right:Expression {
return {
type: 'expression',
left: left,
operator: op,
right: right
};
}
/ Term
/ FunctionTerm
/ GroupedExpression
GroupedExpression
= "(" Whitespace expr:Expression Whitespace ")" {
return {
type: 'group',
body: expr
};
}
Term
= variable:Variable Whitespace operator:ComparisonOperator Whitespace value:Value {
return {
type: 'term',
variable: variable,
operator: operator,
value: value
};
}
FunctionName
= "in"i { return 'in'; }
/ "!in"i { return '!in'; }
FunctionTerm
= variable:Variable Whitespace functionName:FunctionName Whitespace "(" Whitespace head:Value tail:(Whitespace "," Whitespace value:Value { return value; })* Whitespace ")" {
return {
type: 'functionTerm',
variable: variable,
function: functionName,
args: [head].concat(tail)
};
}
LogicalOperator
= "and"i { return 'and'; }
/ "or"i { return 'or'; }
ComparisonOperator
= "=" { return '='; }
/ "!=" { return '!='; }
/ ">=" { return '>='; }
/ "<=" { return '<='; }
/ ">" { return '>'; }
/ "<" { return '<'; }
/ "startswith" { return 'startswith'; }
/ "endswith" { return 'endswith'; }
/ "contains" { return 'contains'; }
/ "!startswith" { return '!startswith'; }
/ "!endswith" { return '!endswith'; }
/ "!contains" { return '!contains'; }
/ "~" { return 'contains'; }
/ "!~" { return '!contains'; }
Variable
= IdentifierCharacter+ { return text(); }
Value
= Boolean
/ Number
/ String
Boolean
= "true" { return true; }
/ "false" { return false; }
Number
= Float
/ Integer
Integer
= Digit+ { return parseInt(text(), 10); }
Float
= left:Digit+ "." right:Digit+ { return parseFloat(text()); }
String
= '"' chars:(EscapedChar / NormalChar)* '"' { return chars.join(''); }
EscapedChar
= '\\"' { return '"'; } // Return the actual character, not the escape sequence
NormalChar
= [^"] // Any character that is not a quote
Digit
= [0-9]
IdentifierCharacter
= [a-zA-Z0-9_$]
Whitespace
= [ \t\n\r]*