expression-language
Version:
Javascript implementation of symfony/expression-language
1,458 lines (1,425 loc) • 140 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ExpressionLanguage = {}));
})(this, (function (exports) { 'use strict';
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 _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);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
const getEditDistance = function (a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
let matrix = [];
// increment along the first column of each row
let i;
for (i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
// increment each column in the first row
let j;
for (j = 0; j <= a.length; j++) {
if (matrix[0] === undefined) {
matrix[0] = [];
}
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for (i = 1; i <= b.length; i++) {
for (j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1,
// substitution
Math.min(matrix[i][j - 1] + 1,
// insertion
matrix[i - 1][j] + 1)); // deletion
}
}
}
if (matrix[b.length] === undefined) {
matrix[b.length] = [];
}
return matrix[b.length][a.length];
};
class SyntaxError extends Error {
constructor(message, cursor, expression, subject, proposals) {
super(message);
this.name = "SyntaxError";
this.cursor = cursor;
this.expression = expression;
this.subject = subject;
this.proposals = proposals;
}
toString() {
let message = `${this.name}: ${this.message} around position ${this.cursor}`;
if (this.expression) {
message = message + ` for expression \`${this.expression}\``;
}
message += ".";
if (this.subject && this.proposals) {
let minScore = Number.MAX_SAFE_INTEGER,
guess = null;
for (let proposal of this.proposals) {
let distance = getEditDistance(this.subject, proposal);
if (distance < minScore) {
guess = proposal;
minScore = distance;
}
}
if (guess !== null && minScore < 3) {
message += ` Did you mean "${guess}"?`;
}
}
return message;
}
}
class TokenStream {
constructor(expression, tokens) {
_defineProperty(this, "next", () => {
this.position += 1;
if (this.tokens[this.position] === undefined) {
throw new SyntaxError("Unexpected end of expression", this.last.cursor, this.expression);
}
});
_defineProperty(this, "expect", (type, value, message) => {
let token = this.current;
if (!token.test(type, value)) {
let compiledMessage = "";
if (message) {
compiledMessage = message + ". ";
}
let valueMessage = "";
if (value) {
valueMessage = ` with value "${value}"`;
}
compiledMessage += `Unexpected token "${token.type}" of value "${token.value}" ("${type}" expected${valueMessage})`;
throw new SyntaxError(compiledMessage, token.cursor, this.expression);
}
this.next();
});
_defineProperty(this, "isEOF", () => {
return Token.EOF_TYPE === this.current.type;
});
_defineProperty(this, "isEqualTo", ts => {
if (ts === null || ts === undefined || !ts instanceof TokenStream) {
return false;
}
if (ts.tokens.length !== this.tokens.length) {
return false;
}
let tsStartPosition = ts.position;
ts.position = 0;
let allTokensMatch = true;
for (let token of this.tokens) {
let match = ts.current.isEqualTo(token);
if (!match) {
allTokensMatch = false;
break;
}
if (ts.position < ts.tokens.length - 1) {
ts.next();
}
}
ts.position = tsStartPosition;
return allTokensMatch;
});
_defineProperty(this, "diff", ts => {
let diff = [];
if (!this.isEqualTo(ts)) {
let index = 0;
let tsStartPosition = ts.position;
ts.position = 0;
for (let token of this.tokens) {
let tokenDiff = token.diff(ts.current);
if (tokenDiff.length > 0) {
diff.push({
index: index,
diff: tokenDiff
});
}
if (ts.position < ts.tokens.length - 1) {
ts.next();
}
}
ts.position = tsStartPosition;
}
return diff;
});
this.expression = expression;
this.position = 0;
this.tokens = tokens;
}
get current() {
return this.tokens[this.position];
}
get last() {
return this.tokens[this.position - 1];
}
toString() {
return this.tokens.join("\n");
}
}
class Token {
constructor(_type, _value, cursor) {
_defineProperty(this, "test", (type, value = null) => {
return this.type === type && (null === value || this.value === value);
});
_defineProperty(this, "isEqualTo", t => {
if (t === null || t === undefined || !t instanceof Token) {
return false;
}
return t.value == this.value && t.type === this.type && t.cursor === this.cursor;
});
_defineProperty(this, "diff", t => {
let diff = [];
if (!this.isEqualTo(t)) {
if (t.value !== this.value) {
diff.push(`Value: ${t.value} != ${this.value}`);
}
if (t.cursor !== this.cursor) {
diff.push(`Cursor: ${t.cursor} != ${this.cursor}`);
}
if (t.type !== this.type) {
diff.push(`Type: ${t.type} != ${this.type}`);
}
}
return diff;
});
this.value = _value;
this.type = _type;
this.cursor = cursor;
}
toString() {
return `${this.cursor} [${this.type}] ${this.value}`;
}
}
_defineProperty(Token, "EOF_TYPE", 'end of expression');
_defineProperty(Token, "NAME_TYPE", 'name');
_defineProperty(Token, "NUMBER_TYPE", 'number');
_defineProperty(Token, "STRING_TYPE", 'string');
_defineProperty(Token, "OPERATOR_TYPE", 'operator');
_defineProperty(Token, "PUNCTUATION_TYPE", 'punctuation');
function tokenize(expression) {
expression = expression.replace(/\r|\n|\t|\v|\f/g, ' ');
let cursor = 0,
tokens = [],
brackets = [],
end = expression.length;
while (cursor < end) {
if (' ' === expression[cursor]) {
++cursor;
continue;
}
let number = extractNumber(expression.substr(cursor));
if (number !== null) {
// numbers
const numberLength = number.length;
if (number.indexOf(".") === -1) {
number = parseInt(number);
} else {
number = parseFloat(number);
}
tokens.push(new Token(Token.NUMBER_TYPE, number, cursor + 1));
cursor += numberLength;
} else {
if ('([{'.indexOf(expression[cursor]) >= 0) {
// opening bracket
brackets.push([expression[cursor], cursor]);
tokens.push(new Token(Token.PUNCTUATION_TYPE, expression[cursor], cursor + 1));
++cursor;
} else {
if (')]}'.indexOf(expression[cursor]) >= 0) {
if (brackets.length === 0) {
throw new SyntaxError(`Unexpected "${expression[cursor]}"`, cursor, expression);
}
let [expect, cur] = brackets.pop(),
matchExpect = expect.replace("(", ")").replace("{", "}").replace("[", "]");
if (expression[cursor] !== matchExpect) {
throw new SyntaxError(`Unclosed "${expect}"`, cur, expression);
}
tokens.push(new Token(Token.PUNCTUATION_TYPE, expression[cursor], cursor + 1));
++cursor;
} else {
let str = extractString(expression.substr(cursor));
if (str !== null) {
//console.log("adding string: " + str);
tokens.push(new Token(Token.STRING_TYPE, str.captured, cursor + 1));
cursor += str.length;
//console.log(`Extracted string: ${str.captured}; Remaining: ${expression.substr(cursor)}`, cursor, expression);
} else {
let operator = extractOperator(expression.substr(cursor));
if (operator) {
tokens.push(new Token(Token.OPERATOR_TYPE, operator, cursor + 1));
cursor += operator.length;
} else {
if (".,?:".indexOf(expression[cursor]) >= 0) {
tokens.push(new Token(Token.PUNCTUATION_TYPE, expression[cursor], cursor + 1));
++cursor;
} else {
let name = extractName(expression.substr(cursor));
if (name) {
tokens.push(new Token(Token.NAME_TYPE, name, cursor + 1));
cursor += name.length;
//console.log(`Extracted name: ${name}; Remaining: ${expression.substr(cursor)}`, cursor, expression)
} else {
throw new SyntaxError(`Unexpected character "${expression[cursor]}"`, cursor, expression);
}
}
}
}
}
}
}
}
tokens.push(new Token(Token.EOF_TYPE, null, cursor + 1));
if (brackets.length > 0) {
let [expect, cur] = brackets.pop();
throw new SyntaxError(`Unclosed "${expect}"`, cur, expression);
}
return new TokenStream(expression, tokens);
}
function extractNumber(str) {
let extracted = null;
let matches = str.match(/^[0-9]+(?:.[0-9]+)?/);
if (matches && matches.length > 0) {
extracted = matches[0];
}
return extracted;
}
const strRegex = /^"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/s;
/**
*
* @param str
* @returns {null|string}
*/
function extractString(str) {
let extracted = null;
if (["'", '"'].indexOf(str.substr(0, 1)) === -1) {
return extracted;
}
let m = strRegex.exec(str);
if (m !== null && m.length > 0) {
if (m[1]) {
extracted = {
captured: m[1]
};
} else {
extracted = {
captured: m[2]
};
}
extracted.length = m[0].length;
}
return extracted;
}
const operators = ["&&", "and", "||", "or",
// Binary
"+", "-", "**", "*", "/", "%",
// Arithmetic
"&", "|", "^",
// Bitwise
"===", "!==", "!=", "==", "<=", ">=", "<", ">",
// Comparison
"contains", "matches", "starts with", "ends with", "not in", "in", "not", "!", "~",
// String concatenation,
'..' // Range function
];
const wordBasedOperators = ["and", "or", "matches", "contains", "starts with", "ends with", "not in", "in", "not"];
/**
*
* @param str
* @returns {null|string}
*/
function extractOperator(str) {
let extracted = null;
for (let operator of operators) {
if (str.substr(0, operator.length) === operator) {
// If it is one of the word based operators, make sure there is a space after it
if (wordBasedOperators.indexOf(operator) >= 0) {
if (str.substr(0, operator.length + 1) === operator + " ") {
extracted = operator;
}
} else {
extracted = operator;
}
break;
}
}
return extracted;
}
function extractName(str) {
let extracted = null;
let matches = str.match(/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/);
if (matches && matches.length > 0) {
extracted = matches[0];
}
return extracted;
}
function is_scalar(mixedVar) {
// eslint-disable-line camelcase
// discuss at: https://locutus.io/php/is_scalar/
// original by: Paulo Freitas
// example 1: is_scalar(186.31)
// returns 1: true
// example 2: is_scalar({0: 'Kevin van Zonneveld'})
// returns 2: false
return /boolean|number|string/.test(typeof mixedVar);
}
function addcslashes(str, charlist) {
// discuss at: https://locutus.io/php/addcslashes/
// original by: Brett Zamir (https://brett-zamir.me)
// note 1: We show double backslashes in the return value example
// note 1: code below because a JavaScript string will not
// note 1: render them as backslashes otherwise
// example 1: addcslashes('foo[ ]', 'A..z'); // Escape all ASCII within capital A to lower z range, including square brackets
// returns 1: "\\f\\o\\o\\[ \\]"
// example 2: addcslashes("zoo['.']", 'z..A'); // Only escape z, period, and A here since not a lower-to-higher range
// returns 2: "\\zoo['\\.']"
// _example 3: addcslashes("@a\u0000\u0010\u00A9", "\0..\37!@\177..\377"); // Escape as octals those specified and less than 32 (0x20) or greater than 126 (0x7E), but not otherwise
// _returns 3: '\\@a\\000\\020\\302\\251'
// _example 4: addcslashes("\u0020\u007E", "\40..\175"); // Those between 32 (0x20 or 040) and 126 (0x7E or 0176) decimal value will be backslashed if specified (not octalized)
// _returns 4: '\\ ~'
// _example 5: addcslashes("\r\u0007\n", '\0..\37'); // Recognize C escape sequences if specified
// _returns 5: "\\r\\a\\n"
// _example 6: addcslashes("\r\u0007\n", '\0'); // Do not recognize C escape sequences if not specified
// _returns 6: "\r\u0007\n"
var target = '';
var chrs = [];
var i = 0;
var j = 0;
var c = '';
var next = '';
var rangeBegin = '';
var rangeEnd = '';
var chr = '';
var begin = 0;
var end = 0;
var octalLength = 0;
var postOctalPos = 0;
var cca = 0;
var escHexGrp = [];
var encoded = '';
var percentHex = /%([\dA-Fa-f]+)/g;
var _pad = function (n, c) {
if ((n = n + '').length < c) {
return new Array(++c - n.length).join('0') + n;
}
return n;
};
for (i = 0; i < charlist.length; i++) {
c = charlist.charAt(i);
next = charlist.charAt(i + 1);
if (c === '\\' && next && /\d/.test(next)) {
// Octal
rangeBegin = charlist.slice(i + 1).match(/^\d+/)[0];
octalLength = rangeBegin.length;
postOctalPos = i + octalLength + 1;
if (charlist.charAt(postOctalPos) + charlist.charAt(postOctalPos + 1) === '..') {
// Octal begins range
begin = rangeBegin.charCodeAt(0);
if (/\\\d/.test(charlist.charAt(postOctalPos + 2) + charlist.charAt(postOctalPos + 3))) {
// Range ends with octal
rangeEnd = charlist.slice(postOctalPos + 3).match(/^\d+/)[0];
// Skip range end backslash
i += 1;
} else if (charlist.charAt(postOctalPos + 2)) {
// Range ends with character
rangeEnd = charlist.charAt(postOctalPos + 2);
} else {
throw new Error('Range with no end point');
}
end = rangeEnd.charCodeAt(0);
if (end > begin) {
// Treat as a range
for (j = begin; j <= end; j++) {
chrs.push(String.fromCharCode(j));
}
} else {
// Supposed to treat period, begin and end as individual characters only, not a range
chrs.push('.', rangeBegin, rangeEnd);
}
// Skip dots and range end (already skipped range end backslash if present)
i += rangeEnd.length + 2;
} else {
// Octal is by itself
chr = String.fromCharCode(parseInt(rangeBegin, 8));
chrs.push(chr);
}
// Skip range begin
i += octalLength;
} else if (next + charlist.charAt(i + 2) === '..') {
// Character begins range
rangeBegin = c;
begin = rangeBegin.charCodeAt(0);
if (/\\\d/.test(charlist.charAt(i + 3) + charlist.charAt(i + 4))) {
// Range ends with octal
rangeEnd = charlist.slice(i + 4).match(/^\d+/)[0];
// Skip range end backslash
i += 1;
} else if (charlist.charAt(i + 3)) {
// Range ends with character
rangeEnd = charlist.charAt(i + 3);
} else {
throw new Error('Range with no end point');
}
end = rangeEnd.charCodeAt(0);
if (end > begin) {
// Treat as a range
for (j = begin; j <= end; j++) {
chrs.push(String.fromCharCode(j));
}
} else {
// Supposed to treat period, begin and end as individual characters only, not a range
chrs.push('.', rangeBegin, rangeEnd);
}
// Skip dots and range end (already skipped range end backslash if present)
i += rangeEnd.length + 2;
} else {
// Character is by itself
chrs.push(c);
}
}
for (i = 0; i < str.length; i++) {
c = str.charAt(i);
if (chrs.indexOf(c) !== -1) {
target += '\\';
cca = c.charCodeAt(0);
if (cca < 32 || cca > 126) {
// Needs special escaping
switch (c) {
case '\n':
target += 'n';
break;
case '\t':
target += 't';
break;
case '\u000D':
target += 'r';
break;
case '\u0007':
target += 'a';
break;
case '\v':
target += 'v';
break;
case '\b':
target += 'b';
break;
case '\f':
target += 'f';
break;
default:
// target += _pad(cca.toString(8), 3);break; // Sufficient for UTF-16
encoded = encodeURIComponent(c);
// 3-length-padded UTF-8 octets
if ((escHexGrp = percentHex.exec(encoded)) !== null) {
// already added a slash above:
target += _pad(parseInt(escHexGrp[1], 16).toString(8), 3);
}
while ((escHexGrp = percentHex.exec(encoded)) !== null) {
target += '\\' + _pad(parseInt(escHexGrp[1], 16).toString(8), 3);
}
break;
}
} else {
// Perform regular backslashed escaping
target += c;
}
} else {
// Just add the character unescaped
target += c;
}
}
return target;
}
class Node {
constructor(nodes = {}, attributes = {}) {
_defineProperty(this, "compile", compiler => {
for (let node of Object.values(this.nodes)) {
node.compile(compiler);
}
});
_defineProperty(this, "evaluate", (functions, values) => {
let results = [];
for (let node of Object.values(this.nodes)) {
results.push(node.evaluate(functions, values));
}
return results;
});
_defineProperty(this, "dump", () => {
let dump = "";
for (let v of this.toArray()) {
dump += is_scalar(v) ? v : v.dump();
}
return dump;
});
_defineProperty(this, "dumpString", value => {
return `"${addcslashes(value, "\0\t\"\\")}"`;
});
_defineProperty(this, "isHash", value => {
let expectedKey = 0;
for (let key of Object.keys(value)) {
key = parseInt(key);
if (key !== expectedKey++) {
return true;
}
}
return false;
});
this.name = 'Node';
this.nodes = nodes;
this.attributes = attributes;
}
toString() {
let attributes = [];
for (let name of Object.keys(this.attributes)) {
let oneAttribute = 'null';
if (this.attributes[name]) {
oneAttribute = this.attributes[name].toString();
}
attributes.push(`${name}: '${oneAttribute}'`);
}
let repr = [this.name + "(" + attributes.join(", ")];
if (this.nodes.length > 0) {
for (let node of Object.values(this.nodes)) {
let lines = node.toString().split("\n");
for (let line of lines) {
repr.push(" " + line);
}
}
repr.push(")");
} else {
repr[0] += ")";
}
return repr.join("\n");
}
toArray() {
throw new Error(`Dumping a "${this.name}" instance is not supported yet.`);
}
}
function range(start, end) {
let result = [];
for (let i = start; i <= end; i++) {
result.push(i);
}
return result;
}
class BinaryNode extends Node {
constructor(_operator, _left, _right) {
super({
left: _left,
right: _right
}, {
operator: _operator
});
_defineProperty(this, "compile", compiler => {
let operator = this.attributes.operator;
if ('matches' === operator) {
compiler.compile(this.nodes.right).raw(".test(").compile(this.nodes.left).raw(")");
return;
} else if ('contains' === operator) {
compiler.raw('(').compile(this.nodes.left).raw(".toString().toLowerCase().includes(").compile(this.nodes.right).raw(".toString().toLowerCase())");
return;
} else if ('starts with' === operator) {
compiler.raw('(').compile(this.nodes.left).raw(".toString().toLowerCase().startsWith(").compile(this.nodes.right).raw(".toString().toLowerCase())");
return;
} else if ('ends with' === operator) {
compiler.raw('(').compile(this.nodes.left).raw(".toString().toLowerCase().endsWith(").compile(this.nodes.right).raw(".toString().toLowerCase())");
return;
}
if (BinaryNode.functions[operator] !== undefined) {
compiler.raw(`${BinaryNode.functions[operator]}(`).compile(this.nodes.left).raw(", ").compile(this.nodes.right).raw(")");
return;
}
if (BinaryNode.operators[operator] !== undefined) {
operator = BinaryNode.operators[operator];
}
compiler.raw("(").compile(this.nodes.left).raw(' ').raw(operator).raw(' ').compile(this.nodes.right).raw(")");
});
_defineProperty(this, "evaluate", (functions, values) => {
let operator = this.attributes.operator,
left = this.nodes.left.evaluate(functions, values);
//console.log("Evaluating: ", left, operator, right);
if (BinaryNode.functions[operator] !== undefined) {
let right = this.nodes.right.evaluate(functions, values);
switch (operator) {
case 'not in':
return right.indexOf(left) === -1;
case 'in':
return right.indexOf(left) >= 0;
case '..':
return range(left, right);
case '**':
return Math.pow(left, right);
}
}
let right = null;
switch (operator) {
case 'or':
case '||':
if (!left) {
right = this.nodes.right.evaluate(functions, values);
}
return left || right;
case 'and':
case '&&':
if (left) {
right = this.nodes.right.evaluate(functions, values);
}
return left && right;
}
right = this.nodes.right.evaluate(functions, values);
switch (operator) {
case '|':
return left | right;
case '^':
return left ^ right;
case '&':
return left & right;
case '==':
return left == right;
case '===':
return left === right;
case '!=':
return left != right;
case '!==':
return left !== right;
case '<':
return left < right;
case '>':
return left > right;
case '>=':
return left >= right;
case '<=':
return left <= right;
case 'not in':
return right.indexOf(left) === -1;
case 'in':
return right.indexOf(left) >= 0;
case '+':
return left + right;
case '-':
return left - right;
case '~':
return left.toString() + right.toString();
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
case 'matches':
let res = right.match(BinaryNode.regex_expression);
let regexp = new RegExp(res[1], res[2]);
return regexp.test(left);
case 'contains':
return left.toString().toLowerCase().includes(right.toString().toLowerCase());
case 'starts with':
return left.toString().toLowerCase().startsWith(right.toString().toLowerCase());
case 'ends with':
return left.toString().toLowerCase().endsWith(right.toString().toLowerCase());
}
});
this.name = "BinaryNode";
}
toArray() {
return ["(", this.nodes.left, ' ' + this.attributes.operator + ' ', this.nodes.right, ")"];
}
}
_defineProperty(BinaryNode, "regex_expression", /\/(.+)\/(.*)/);
_defineProperty(BinaryNode, "operators", {
'~': '.',
'and': '&&',
'or': '||'
});
_defineProperty(BinaryNode, "functions", {
'**': 'Math.pow',
'..': 'range',
'in': 'includes',
'not in': '!includes'
});
class UnaryNode extends Node {
constructor(operator, node) {
super({
node: node
}, {
operator: operator
});
_defineProperty(this, "compile", compiler => {
compiler.raw('(').raw(UnaryNode.operators[this.attributes.operator]).compile(this.nodes.node).raw(')');
});
_defineProperty(this, "evaluate", (functions, values) => {
let value = this.nodes.node.evaluate(functions, values);
switch (this.attributes.operator) {
case 'not':
case '!':
return !value;
case '-':
return -value;
}
return value;
});
this.name = 'UnaryNode';
}
toArray() {
return ['(', this.attributes.operator + " ", this.nodes.node, ')'];
}
}
_defineProperty(UnaryNode, "operators", {
'!': '!',
'not': '!',
'+': '+',
'-': '-'
});
class ConstantNode extends Node {
constructor(_value, isIdentifier = false) {
super({}, {
value: _value
});
_defineProperty(this, "compile", compiler => {
compiler.repr(this.attributes.value, this.isIdentifier);
});
_defineProperty(this, "evaluate", (functions, values) => {
return this.attributes.value;
});
_defineProperty(this, "toArray", () => {
let array = [],
value = this.attributes.value;
if (this.isIdentifier) {
array.push(value);
} else if (true === value) {
array.push('true');
} else if (false === value) {
array.push('false');
} else if (null === value) {
array.push('null');
} else if (typeof value === "number") {
array.push(value);
} else if (typeof value === "string") {
array.push(this.dumpString(value));
} else if (Array.isArray(value)) {
for (let v of value) {
array.push(',');
array.push(new ConstantNode(v));
}
array[0] = '[';
array.push(']');
} else if (this.isHash(value)) {
for (let k of Object.keys(value)) {
array.push(', ');
array.push(new ConstantNode(k));
array.push(': ');
array.push(new ConstantNode(value[k]));
}
array[0] = '{';
array.push('}');
}
return array;
});
this.isIdentifier = isIdentifier;
this.name = 'ConstantNode';
}
}
class ConditionalNode extends Node {
constructor(expr1, expr2, expr3) {
super({
expr1: expr1,
expr2: expr2,
expr3: expr3
});
_defineProperty(this, "compile", compiler => {
compiler.raw('((').compile(this.nodes.expr1).raw(') ? (').compile(this.nodes.expr2).raw(') : (').compile(this.nodes.expr3).raw('))');
});
_defineProperty(this, "evaluate", (functions, values) => {
if (this.nodes.expr1.evaluate(functions, values)) {
return this.nodes.expr2.evaluate(functions, values);
}
return this.nodes.expr3.evaluate(functions, values);
});
this.name = 'ConditionalNode';
}
toArray() {
return ['(', this.nodes.expr1, ' ? ', this.nodes.expr2, ' : ', this.nodes.expr3, ')'];
}
}
class FunctionNode extends Node {
constructor(name, _arguments2) {
//console.log("Creating function node: ", name, _arguments);
super({
arguments: _arguments2
}, {
name: name
});
_defineProperty(this, "compile", compiler => {
let _arguments = [];
for (let node of Object.values(this.nodes.arguments.nodes)) {
_arguments.push(compiler.subcompile(node));
}
let fn = compiler.getFunction(this.attributes.name);
compiler.raw(fn.compiler.apply(null, _arguments));
});
_defineProperty(this, "evaluate", (functions, values) => {
let _arguments = [values];
for (let node of Object.values(this.nodes.arguments.nodes)) {
//console.log("Testing: ", node, functions, values);
_arguments.push(node.evaluate(functions, values));
}
return functions[this.attributes.name]['evaluator'].apply(null, _arguments);
});
this.name = 'FunctionNode';
}
toArray() {
let array = [];
array.push(this.attributes.name);
for (let node of Object.values(this.nodes.arguments.nodes)) {
array.push(', ');
array.push(node);
}
array[1] = '(';
array.push(')');
return array;
}
}
class NameNode extends Node {
constructor(name) {
super({}, {
name: name
});
_defineProperty(this, "compile", compiler => {
compiler.raw(this.attributes.name);
});
_defineProperty(this, "evaluate", (functions, values) => {
//console.log(`Checking for value of "${this.attributes.name}"`);
let value = values[this.attributes.name];
//console.log(`Value: ${value}`);
return value;
});
this.name = 'NameNode';
}
toArray() {
return [this.attributes.name];
}
}
class ArrayNode extends Node {
constructor() {
super();
_defineProperty(this, "addElement", (value, key = null) => {
if (null === key) {
key = new ConstantNode(++this.index);
} else {
if (this.type === 'Array') {
this.type = 'Object';
}
}
this.nodes[(++this.keyIndex).toString()] = key;
this.nodes[(++this.keyIndex).toString()] = value;
});
_defineProperty(this, "compile", compiler => {
if (this.type === 'Object') {
compiler.raw('{');
} else {
compiler.raw('[');
}
this.compileArguments(compiler, this.type !== "Array");
if (this.type === 'Object') {
compiler.raw('}');
} else {
compiler.raw(']');
}
});
_defineProperty(this, "evaluate", (functions, values) => {
let result;
if (this.type === 'Array') {
result = [];
for (let pair of this.getKeyValuePairs()) {
result.push(pair.value.evaluate(functions, values));
}
} else {
result = {};
for (let pair of this.getKeyValuePairs()) {
result[pair.key.evaluate(functions, values)] = pair.value.evaluate(functions, values);
}
}
return result;
});
_defineProperty(this, "getKeyValuePairs", () => {
let pairs = [];
let nodes = Object.values(this.nodes);
let i,
j,
pair,
chunk = 2;
for (i = 0, j = nodes.length; i < j; i += chunk) {
pair = nodes.slice(i, i + chunk);
pairs.push({
key: pair[0],
value: pair[1]
});
}
return pairs;
});
_defineProperty(this, "compileArguments", (compiler, withKeys = true) => {
let first = true;
for (let pair of this.getKeyValuePairs()) {
if (!first) {
compiler.raw(', ');
}
first = false;
if (withKeys) {
compiler.compile(pair.key).raw(': ');
}
compiler.compile(pair.value);
}
});
this.name = "ArrayNode";
this.type = "Array";
this.index = -1;
this.keyIndex = -1;
}
toArray() {
let value = {};
for (let pair of this.getKeyValuePairs()) {
value[pair.key.attributes.value] = pair.value;
}
let array = [];
if (this.isHash(value)) {
for (let k of Object.keys(value)) {
array.push(', ');
array.push(new ConstantNode(k));
array.push(': ');
array.push(value[k]);
}
array[0] = '{';
array.push('}');
} else {
for (let v of Object.values(value)) {
array.push(', ');
array.push(v);
}
array[0] = '[';
array.push(']');
}
return array;
}
}
class ArgumentsNode extends ArrayNode {
constructor() {
super();
_defineProperty(this, "compile", compiler => {
this.compileArguments(compiler, false);
});
this.name = "ArgumentsNode";
}
toArray() {
let array = [];
for (let pair of this.getKeyValuePairs()) {
array.push(pair.value);
array.push(", ");
}
array.pop();
return array;
}
}
class GetAttrNode extends Node {
constructor(node, attribute, _arguments, type) {
super({
node: node,
attribute: attribute,
arguments: _arguments
}, {
type: type
});
_defineProperty(this, "compile", compiler => {
switch (this.attributes.type) {
case GetAttrNode.PROPERTY_CALL:
compiler.compile(this.nodes.node).raw('.').raw(this.nodes.attribute.attributes.value);
break;
case GetAttrNode.METHOD_CALL:
compiler.compile(this.nodes.node).raw('.').raw(this.nodes.attribute.attributes.value).raw('(').compile(this.nodes.arguments).raw(')');
break;
case GetAttrNode.ARRAY_CALL:
compiler.compile(this.nodes.node).raw('[').compile(this.nodes.attribute).raw(']');
break;
}
});
_defineProperty(this, "evaluate", (functions, values) => {
switch (this.attributes.type) {
case GetAttrNode.PROPERTY_CALL:
let obj = this.nodes.node.evaluate(functions, values),
property = this.nodes.attribute.attributes.value;
if (typeof obj !== "object") {
throw new Error(`Unable to get property "${property}" on a non-object: ` + typeof obj);
}
return obj[property];
case GetAttrNode.METHOD_CALL:
let obj2 = this.nodes.node.evaluate(functions, values),
method = this.nodes.attribute.attributes.value;
if (typeof obj2 !== 'object') {
throw new Error(`Unable to call method "${method}" on a non-object: ` + typeof obj2);
}
if (obj2[method] === undefined) {
throw new Error(`Method "${method}" is undefined on object.`);
}
if (typeof obj2[method] != 'function') {
throw new Error(`Method "${method}" is not a function on object.`);
}
let evaluatedArgs = this.nodes.arguments.evaluate(functions, values);
return obj2[method].apply(null, evaluatedArgs);
case GetAttrNode.ARRAY_CALL:
let array = this.nodes.node.evaluate(functions, values);
if (!Array.isArray(array) && typeof array !== 'object') {
throw new Error(`Unable to get an item on a non-array: ` + typeof array);
}
return array[this.nodes.attribute.evaluate(functions, values)];
}
});
this.name = 'GetAttrNode';
}
toArray() {
switch (this.attributes.type) {
case GetAttrNode.PROPERTY_CALL:
return [this.nodes.node, '.', this.nodes.attribute];
case GetAttrNode.METHOD_CALL:
return [this.nodes.node, '.', this.nodes.attribute, '(', this.nodes.arguments, ')'];
case GetAttrNode.ARRAY_CALL:
return [this.nodes.node, '[', this.nodes.attribute, ']'];
}
}
}
_defineProperty(GetAttrNode, "PROPERTY_CALL", 1);
_defineProperty(GetAttrNode, "METHOD_CALL", 2);
_defineProperty(GetAttrNode, "ARRAY_CALL", 3);
const OPERATOR_LEFT = 1;
const 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(`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(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(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(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(token.value, expr));
}
if (token.test(Token.PUNCTUATION_TYPE, "(")) {
//console.log("Found '('.", token.type, token.value);
this.tokenStream.next();
let expr = this.parseExpression();
this.tokenStream.expect(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(Token.PUNCTUATION_TYPE, '[', 'An array element was expected');
let node = new ArrayNode(),
first = true;
while (!this.tokenStream.current.test(Token.PUNCTUATION_TYPE, ']')) {
if (!first) {
this.tokenStream.expect(Token.PUNCTUATION_TYPE, ",", "An array element must be followed by a comma");
// trailing ,?
if (this.tokenStream.current.test(Token.PUNCTUATION_TYPE, "]")) {
break;
}
}
first = false;
node.addElement(this.parseExpression());
}
this.tokenStream.expect(Token.PUNCTUATION_TYPE, "]", "An opened array is not properly closed");
return node;
});
_defineProperty(this, "parseHashExpression", () => {
this.tokenStream.expect(Token.PUNCTUATION_TYPE, "{", "A hash element was expected");
let node = new ArrayNode(),
first = true;
while (!this.tokenStream.current.test(Token.PUNCTUATION_TYPE, '}')) {
if (!first) {
this.tokenStream.expect(Token.PUNCTUATION_TYPE, ",", "An array element must be followed by a comma");
// trailing ,?
if (this.tokenStream.current.test(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(Token.STRING_TYPE) || this.tokenStream.current.test(Token.NAME_TYPE) || this.tokenStream.current.test(Token.NUMBER_TYPE)) {
key = new ConstantNode(this.tokenStream.current.value);
this.tokenStream.next();
} else if (this.tokenStream.current.test(Token.PUNCTUATION_TYPE, "(")) {
key = this.parseExpression();
} else {
let current = this.tokenStream.current;
throw new SyntaxError(`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(Token.PUNCTUATION_TYPE, ":", "A hash key must be followed by a colon (:)");
let value = this.parseExpression();
node.addElement(value, key);
}
this.tokenStream.expect(Token.PUNCTUATION_TYPE, "}", "An opened hash is not properly closed");
return node;
});
_defineProperty(this, "parsePostfixExpression", node => {
let token = this.tokenStream.current;
while (Token.PUNCTUATION_TYPE === token.type) {
if ('.' === token.value) {
this.tokenStream.next();
token = this.tokenStream.current;
this.tokenStream.next();
if (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.
Token.OPERATOR_TYPE !== token.type || !/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/.test(token.value))) {
throw new SyntaxError('Expected name', token.cursor, this.tokenStream.expression);
}
let arg = new ConstantNode(token.value, true),
_arguments = new ArgumentsNode(),
type = null;
if (this.tokenStream.current.test(Token.PUNCTUATION_TYPE, "(")) {
type = GetAttrNode.METHOD_CALL;
for (let n of Object.values(this.parseArguments().nodes)) {
_arguments.addElement(n);
}
} else {
type = GetAttrNode.PROPERTY_CALL;
}
node = new GetAttrNode(node, arg, _arguments, type);
} else if ('[' === token.value) {
this.tokenStream.next();
let arg = this.parseExpression();
this.tokenStream.expect(Token.PUNCTUATION_TYPE, "]");
node = new GetAttrNode(node, arg, new ArgumentsNode(), GetAttrNode.ARRAY_CALL);
} else {
break;
}
token = this.tokenStream.current;
}
return node;
});
_defineProperty(th