expression-language
Version:
Javascript implementation of symfony/expression-language
433 lines (426 loc) • 17.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.OPERATOR_RIGHT = exports.OPERATOR_LEFT = void 0;
var _SyntaxError = _interopRequireDefault(require("./SyntaxError"));
var _TokenStream = require("./TokenStream");
var _Node = _interopRequireDefault(require("./Node/Node"));
var _BinaryNode = _interopRequireDefault(require("./Node/BinaryNode"));
var _UnaryNode = _interopRequireDefault(require("./Node/UnaryNode"));
var _ConstantNode = _interopRequireDefault(require("./Node/ConstantNode"));
var _ConditionalNode = _interopRequireDefault(require("./Node/ConditionalNode"));
var _FunctionNode = _interopRequireDefault(require("./Node/FunctionNode"));
var _NameNode = _interopRequireDefault(require("./Node/NameNode"));
var _ArrayNode = _interopRequireDefault(require("./Node/ArrayNode"));
var _ArgumentsNode = _interopRequireDefault(require("./Node/ArgumentsNode"));
var _GetAttrNode = _interopRequireDefault(require("./Node/GetAttrNode"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const OPERATOR_LEFT = exports.OPERATOR_LEFT = 1;
const OPERATOR_RIGHT = exports.OPERATOR_RIGHT = 2;
class Parser {
constructor(functions = {}) {
_defineProperty(this, "functions", {});
_defineProperty(this, "unaryOperators", {
'not': {
'precedence': 50
},
'!': {
'precedence': 50
},
'-': {
'precedence': 500
},
'+': {
'precedence': 500
}
});
_defineProperty(this, "binaryOperators", {
'or': {
'precedence': 10,
'associativity': OPERATOR_LEFT
},
'||': {
'precedence': 10,
'associativity': OPERATOR_LEFT
},
'and': {
'precedence': 15,
'associativity': OPERATOR_LEFT
},
'&&': {
'precedence': 15,
'associativity': OPERATOR_LEFT
},
'|': {
'precedence': 16,
'associativity': OPERATOR_LEFT
},
'^': {
'precedence': 17,
'associativity': OPERATOR_LEFT
},
'&': {
'precedence': 18,
'associativity': OPERATOR_LEFT
},
'==': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'===': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'!=': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'!==': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'<': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'>': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'>=': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'<=': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'not in': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'in': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'matches': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'contains': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'starts with': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'ends with': {
'precedence': 20,
'associativity': OPERATOR_LEFT
},
'..': {
'precedence': 25,
'associativity': OPERATOR_LEFT
},
'+': {
'precedence': 30,
'associativity': OPERATOR_LEFT
},
'-': {
'precedence': 30,
'associativity': OPERATOR_LEFT
},
'~': {
'precedence': 40,
'associativity': OPERATOR_LEFT
},
'*': {
'precedence': 60,
'associativity': OPERATOR_LEFT
},
'/': {
'precedence': 60,
'associativity': OPERATOR_LEFT
},
'%': {
'precedence': 60,
'associativity': OPERATOR_LEFT
},
'**': {
'precedence': 200,
'associativity': OPERATOR_RIGHT
}
});
_defineProperty(this, "parse", (tokenStream, names = []) => {
this.tokenStream = tokenStream;
this.names = names;
this.objectMatches = {};
this.cachedNames = null;
this.nestedExecutions = 0;
//console.log("tokens: ", tokenStream.toString());
let node = this.parseExpression();
if (!this.tokenStream.isEOF()) {
throw new _SyntaxError.default(`Unexpected token "${this.tokenStream.current.type}" of value "${this.tokenStream.current.value}".`, this.tokenStream.current.cursor, this.tokenStream.expression);
}
return node;
});
_defineProperty(this, "parseExpression", (precedence = 0) => {
let expr = this.getPrimary();
let token = this.tokenStream.current;
this.nestedExecutions++;
if (this.nestedExecutions > 100) {
throw new Error("Way to many executions on '" + token.toString() + "' of '" + this.tokenStream.toString() + "'");
}
//console.log("Parsing: ", token);
while (token.test(_TokenStream.Token.OPERATOR_TYPE) && this.binaryOperators[token.value] !== undefined && this.binaryOperators[token.value] !== null && this.binaryOperators[token.value].precedence >= precedence) {
let op = this.binaryOperators[token.value];
this.tokenStream.next();
let expr1 = this.parseExpression(OPERATOR_LEFT === op.associativity ? op.precedence + 1 : op.precedence);
expr = new _BinaryNode.default(token.value, expr, expr1);
token = this.tokenStream.current;
}
if (0 === precedence) {
return this.parseConditionalExpression(expr);
}
return expr;
});
_defineProperty(this, "getPrimary", () => {
let token = this.tokenStream.current;
if (token.test(_TokenStream.Token.OPERATOR_TYPE) && this.unaryOperators[token.value] !== undefined && this.unaryOperators[token.value] !== null) {
let operator = this.unaryOperators[token.value];
this.tokenStream.next();
let expr = this.parseExpression(operator.precedence);
return this.parsePostfixExpression(new _UnaryNode.default(token.value, expr));
}
if (token.test(_TokenStream.Token.PUNCTUATION_TYPE, "(")) {
//console.log("Found '('.", token.type, token.value);
this.tokenStream.next();
let expr = this.parseExpression();
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ")", "An opened parenthesis is not properly closed");
return this.parsePostfixExpression(expr);
}
return this.parsePrimaryExpression();
});
_defineProperty(this, "hasVariable", name => {
return this.getNames().indexOf(name) >= 0;
});
_defineProperty(this, "getNames", () => {
if (this.cachedNames !== null) {
return this.cachedNames;
}
if (this.names && this.names.length > 0) {
let names = [];
let index = 0;
this.objectMatches = {};
for (let name of this.names) {
if (typeof name === "object") {
this.objectMatches[Object.values(name)[0]] = index;
names.push(Object.keys(name)[0]);
names.push(Object.values(name)[0]);
} else {
names.push(name);
}
index++;
}
this.cachedNames = names;
return names;
}
return [];
});
_defineProperty(this, "parseArrayExpression", () => {
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, '[', 'An array element was expected');
let node = new _ArrayNode.default(),
first = true;
while (!this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, ']')) {
if (!first) {
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ",", "An array element must be followed by a comma");
// trailing ,?
if (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, "]")) {
break;
}
}
first = false;
node.addElement(this.parseExpression());
}
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, "]", "An opened array is not properly closed");
return node;
});
_defineProperty(this, "parseHashExpression", () => {
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, "{", "A hash element was expected");
let node = new _ArrayNode.default(),
first = true;
while (!this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, '}')) {
if (!first) {
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ",", "An array element must be followed by a comma");
// trailing ,?
if (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, "}")) {
break;
}
}
first = false;
let key = null;
// a hash key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if (this.tokenStream.current.test(_TokenStream.Token.STRING_TYPE) || this.tokenStream.current.test(_TokenStream.Token.NAME_TYPE) || this.tokenStream.current.test(_TokenStream.Token.NUMBER_TYPE)) {
key = new _ConstantNode.default(this.tokenStream.current.value);
this.tokenStream.next();
} else if (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, "(")) {
key = this.parseExpression();
} else {
let current = this.tokenStream.current;
throw new _SyntaxError.default(`A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "${current.type}" of value "${current.value}"`, current.cursor, this.tokenStream.expression);
}
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ":", "A hash key must be followed by a colon (:)");
let value = this.parseExpression();
node.addElement(value, key);
}
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, "}", "An opened hash is not properly closed");
return node;
});
_defineProperty(this, "parsePostfixExpression", node => {
let token = this.tokenStream.current;
while (_TokenStream.Token.PUNCTUATION_TYPE === token.type) {
if ('.' === token.value) {
this.tokenStream.next();
token = this.tokenStream.current;
this.tokenStream.next();
if (_TokenStream.Token.NAME_TYPE !== token.type && (
// Operators like "not" and "matches" are valid method or property names,
//
// In other words, besides NAME_TYPE, OPERATOR_TYPE could also be parsed as a property or method.
// This is because operators are processed by the lexer prior to names. So "not" in "foo.not()" or "matches" in "foo.matches" will be recognized as an operator first.
// But in fact, "not" and "matches" in such expressions shall be parsed as method or property names.
//
// And this ONLY works if the operator consists of valid characters for a property or method name.
//
// Other types, such as STRING_TYPE and NUMBER_TYPE, can't be parsed as property nor method names.
//
// As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown.
_TokenStream.Token.OPERATOR_TYPE !== token.type || !/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/.test(token.value))) {
throw new _SyntaxError.default('Expected name', token.cursor, this.tokenStream.expression);
}
let arg = new _ConstantNode.default(token.value, true),
_arguments = new _ArgumentsNode.default(),
type = null;
if (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, "(")) {
type = _GetAttrNode.default.METHOD_CALL;
for (let n of Object.values(this.parseArguments().nodes)) {
_arguments.addElement(n);
}
} else {
type = _GetAttrNode.default.PROPERTY_CALL;
}
node = new _GetAttrNode.default(node, arg, _arguments, type);
} else if ('[' === token.value) {
this.tokenStream.next();
let arg = this.parseExpression();
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, "]");
node = new _GetAttrNode.default(node, arg, new _ArgumentsNode.default(), _GetAttrNode.default.ARRAY_CALL);
} else {
break;
}
token = this.tokenStream.current;
}
return node;
});
_defineProperty(this, "parseArguments", () => {
let args = [];
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, "(", "A list of arguments must begin with an opening parenthesis");
while (!this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, ")")) {
if (args.length !== 0) {
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ",", "Arguments must be separated by a comma");
}
args.push(this.parseExpression());
}
this.tokenStream.expect(_TokenStream.Token.PUNCTUATION_TYPE, ")", "A list of arguments must be closed by a parenthesis");
return new _Node.default(args);
});
this.functions = functions;
this.tokenStream = null;
this.names = null;
this.objectMatches = {};
this.cachedNames = null;
this.nestedExecutions = 0;
}
parseConditionalExpression(expr) {
while (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, "?")) {
this.tokenStream.next();
let expr2, expr3;
if (!this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, ":")) {
expr2 = this.parseExpression();
if (this.tokenStream.current.test(_TokenStream.Token.PUNCTUATION_TYPE, ":")) {
this.tokenStream.next();
expr3 = this.parseExpression();
} else {
expr3 = new _ConstantNode.default(null);
}
} else {
this.tokenStream.next();
expr2 = expr;
expr3 = this.parseExpression();
}
expr = new _ConditionalNode.default(expr, expr2, expr3);
}
return expr;
}
parsePrimaryExpression() {
let token = this.tokenStream.current,
node = null;
switch (token.type) {
case _TokenStream.Token.NAME_TYPE:
this.tokenStream.next();
switch (token.value) {
case 'true':
case 'TRUE':
return new _ConstantNode.default(true);
case 'false':
case 'FALSE':
return new _ConstantNode.default(false);
case 'null':
case 'NULL':
return new _ConstantNode.default(null);
default:
if ("(" === this.tokenStream.current.value) {
if (this.functions[token.value] === undefined) {
throw new _SyntaxError.default(`The function "${token.value}" does not exist`, token.cursor, this.tokenStream.expression, token.values, Object.keys(this.functions));
}
node = new _FunctionNode.default(token.value, this.parseArguments());
} else {
if (!this.hasVariable(token.value)) {
throw new _SyntaxError.default(`Variable "${token.value}" is not valid`, token.cursor, this.tokenStream.expression, token.value, this.getNames());
}
let name = token.value;
//console.log("Checking for object matches: ", name, this.objectMatches, this.getNames());
if (this.objectMatches[name] !== undefined) {
name = this.getNames()[this.objectMatches[name]];
}
node = new _NameNode.default(name);
}
}
break;
case _TokenStream.Token.NUMBER_TYPE:
case _TokenStream.Token.STRING_TYPE:
this.tokenStream.next();
return new _ConstantNode.default(token.value);
default:
if (token.test(_TokenStream.Token.PUNCTUATION_TYPE, "[")) {
node = this.parseArrayExpression();
} else if (token.test(_TokenStream.Token.PUNCTUATION_TYPE, "{")) {
node = this.parseHashExpression();
} else {
throw new _SyntaxError.default(`Unexpected token "${token.type}" of value "${token.value}"`, token.cursor, this.tokenStream.expression);
}
}
return this.parsePostfixExpression(node);
}
}
exports.default = Parser;