UNPKG

expression-language

Version:

Javascript implementation of symfony/expression-language

1,458 lines (1,425 loc) 140 kB
(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