UNPKG

expression-language

Version:

Javascript implementation of symfony/expression-language

1,454 lines (1,424 loc) 163 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; } // Skip block comments if (expression.substr(cursor, 2) === '/*') { const endIdx = expression.indexOf('*/', cursor + 2); if (endIdx === -1) { // Unclosed comment: ignore rest of expression cursor = end; break; } else { cursor = endIdx + 2; continue; } } let number = extractNumber(expression.substr(cursor)); if (number !== null) { // numbers const numberLength = number.length; const raw = number; const clean = raw.replace(/_/g, ''); // Decide integer vs float based on presence of decimal point or exponent if (clean.indexOf(".") === -1 && clean.indexOf("e") === -1 && clean.indexOf("E") === -1) { number = parseInt(clean, 10); } else { number = parseFloat(clean); } 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 if (expression.substr(cursor, 2) === "\\\\") { // Two backslashes outside of strings represent a single literal backslash token tokens.push(new Token(Token.PUNCTUATION_TYPE, "\\", cursor + 1)); cursor += 2; } else { // If the previous token is a dot accessor ('.' or '?.'), prefer extracting a name before operators const lastToken = tokens.length > 0 ? tokens[tokens.length - 1] : null; const preferName = lastToken && lastToken.type === Token.PUNCTUATION_TYPE && (lastToken.value === '.' || lastToken.value === '?.'); if (preferName) { let name = extractName(expression.substr(cursor)); if (name) { tokens.push(new Token(Token.NAME_TYPE, name, cursor + 1)); cursor += name.length; } else { let operator = extractOperator(expression.substr(cursor)); if (operator) { tokens.push(new Token(Token.OPERATOR_TYPE, operator, cursor + 1)); cursor += operator.length; } else if (expression.substr(cursor, 2) === '?.' || expression.substr(cursor, 2) === '??') { tokens.push(new Token(Token.PUNCTUATION_TYPE, expression.substr(cursor, 2), cursor + 1)); cursor += 2; } else if (".,?:".indexOf(expression[cursor]) >= 0) { tokens.push(new Token(Token.PUNCTUATION_TYPE, expression[cursor], cursor + 1)); ++cursor; } else { throw new SyntaxError(`Unexpected character "${expression[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 (expression.substr(cursor, 2) === '?.' || expression.substr(cursor, 2) === '??') { tokens.push(new Token(Token.PUNCTUATION_TYPE, expression.substr(cursor, 2), cursor + 1)); cursor += 2; } 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; // Supports: // - integers: 123, 1_000 // - decimals: 123.45, .45, 123., with optional underscores in integer and fraction parts // - exponent: e or E with optional sign and digits (e.g., 1.23e+10, .7_189e10) // Note: underscores are allowed between digits but not at boundaries; we simply capture and // rely on parseFloat/parseInt after removing underscores implicitly by JavaScript (parseFloat ignores underscores only in modern engines, so we will strip them manually before parsing if needed elsewhere). Here we only extract the literal token string; Tokenize later uses parseInt/parseFloat which will work if underscores are removed there. Tests expect numeric values parsed correctly, so we must ensure extraction includes underscores. const numberRegex = /^(?:((?:\d(?:_?\d)*)\.(?:\d(?:_?\d)*)|\.(?:\d(?:_?\d)*)|(?:\d(?:_?\d)*))(?:[eE][+-]?\d(?:_?\d)*)?)/; let matches = str.match(numberRegex); if (matches && matches.length > 0) { extracted = matches[0]; } return extracted; } const strRegex = /^"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/s; function unescapeString(s, quote) { // Only handle escaping of backslash and the matching quote; do NOT translate control sequences (e.g., \n) // Replace escaped quote first, then collapse escaped backslashes if (quote === '"') { s = s.replace(/\\\"/g, '"'); } else if (quote === "'") { s = s.replace(/\\'/g, "'"); } // Replace double backslashes with single backslash s = s.replace(/\\\\/g, '\\'); return 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 (typeof m[1] !== 'undefined') { extracted = { captured: unescapeString(m[1], '"') }; } else { extracted = { captured: unescapeString(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", "!", "xor", "~", // String concatenation, '..' // Range function ]; const wordBasedOperators = ["and", "or", "matches", "contains", "starts with", "ends with", "not in", "in", "not", "xor"]; /** * * @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, "toArray", () => { throw new Error(`Dumping a "${this.name}" instance is not supported yet.`); }); _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"); } } 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; case 'xor': right = this.nodes.right.evaluate(functions, values); return right && !left || left && !right; case '<<': right = this.nodes.right.evaluate(functions, values); return left << right; case '>>': 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': if (left === null || left === undefined) { return false; } 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()); } }); _defineProperty(this, "toArray", () => { return ["(", this.nodes.left, ' ' + this.attributes.operator + ' ', this.nodes.right, ")"]; }); this.name = "BinaryNode"; } } _defineProperty(BinaryNode, "regex_expression", /\/(.+)\/(.*)/); _defineProperty(BinaryNode, "operators", { '~': '.', 'and': '&&', 'or': '||', 'xor': 'xor', '<<': '<<', '>>': '>>' }); _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; case '~': return ~value; } return value; }); _defineProperty(this, "toArray", () => { return ['(', this.attributes.operator + " ", this.nodes.node, ')']; }); this.name = 'UnaryNode'; } } _defineProperty(UnaryNode, "operators", { '!': '!', 'not': '!', '+': '+', '-': '-', '~': '~' }); class ConstantNode extends Node { constructor(_value, isIdentifier = false, isNullSafe = 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.isNullSafe = isNullSafe; 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); }); _defineProperty(this, "toArray", () => { return ['(', this.nodes.expr1, ' ? ', this.nodes.expr2, ' : ', this.nodes.expr3, ')']; }); this.name = 'ConditionalNode'; } } class FunctionNode extends Node { constructor(name, _arguments2) { //console.log("Creating function node: ", name, _arguments); super({ fnArguments: _arguments2 }, { name: name }); _defineProperty(this, "compile", compiler => { let _arguments = []; for (let node of Object.values(this.nodes.fnArguments.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.fnArguments.nodes)) { //console.log("Testing: ", node, functions, values); _arguments.push(node.evaluate(functions, values)); } return functions[this.attributes.name]['evaluator'].apply(null, _arguments); }); _defineProperty(this, "toArray", () => { let array = []; array.push(this.attributes.name); for (let node of Object.values(this.nodes.fnArguments.nodes)) { array.push(', '); array.push(node); } array[1] = '('; array.push(')'); return array; }); this.name = 'FunctionNode'; } } 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; }); _defineProperty(this, "toArray", () => { return [this.attributes.name]; }); this.name = 'NameNode'; } } 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, "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; }); _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; } } class ArgumentsNode extends ArrayNode { constructor() { super(); _defineProperty(this, "compile", compiler => { this.compileArguments(compiler, false); }); _defineProperty(this, "toArray", () => { let array = []; for (let pair of this.getKeyValuePairs()) { array.push(pair.value); array.push(", "); } array.pop(); return array; }); this.name = "ArgumentsNode"; } } class GetAttrNode extends Node { constructor(node, attribute, fnArguments, type) { super({ node: node, attribute: attribute, fnArguments: fnArguments }, { type: type, is_null_coalesce: false, is_short_circuited: false }); _defineProperty(this, "compile", compiler => { const nullSafe = this.nodes.attribute instanceof ConstantNode && this.nodes.attribute.isNullSafe; switch (this.attributes.type) { case GetAttrNode.PROPERTY_CALL: compiler.compile(this.nodes.node).raw(nullSafe ? '?.' : '.').raw(this.nodes.attribute.attributes.value); break; case GetAttrNode.METHOD_CALL: compiler.compile(this.nodes.node).raw(nullSafe ? '?.' : '.').raw(this.nodes.attribute.attributes.value).raw('(').compile(this.nodes.fnArguments).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); if (null === obj && (this.nodes.attribute.isNullSafe || this.attributes.is_null_coalesce)) { this.attributes.is_short_circuited = true; return null; } if (null === obj && this.isShortCircuited()) { return null; } if (typeof obj !== "object") { throw new Error(`Unable to get property "${property}" on a non-object: ` + typeof obj); } let property = this.nodes.attribute.attributes.value; if (this.attributes.is_null_coalesce) { return obj[property] ?? null; } return obj[property]; case GetAttrNode.METHOD_CALL: let obj2 = this.nodes.node.evaluate(functions, values); if (null === obj2 && this.nodes.attribute.isNullSafe) { this.attributes.is_short_circuited = true; return null; } if (null === obj2 && this.isShortCircuited()) { return null; } let 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.fnArguments.evaluate(functions, values); return obj2[method].apply(null, evaluatedArgs); case GetAttrNode.ARRAY_CALL: let array = this.nodes.node.evaluate(functions, values); if (null === array && this.isShortCircuited()) { return null; } if (!Array.isArray(array) && typeof array !== 'object' && !(null === array && this.attributes.is_null_coalesce)) { throw new Error(`Unable to get an item on a non-array: ` + typeof array); } if (this.attributes.is_null_coalesce) { if (!array) { return null; } return array[this.nodes.attribute.evaluate(functions, values)] ?? null; } return array[this.nodes.attribute.evaluate(functions, values)]; } }); _defineProperty(this, "toArray", () => { const nullSafe = this.nodes.attribute instanceof ConstantNode && this.nodes.attribute.isNullSafe; switch (this.attributes.type) { case GetAttrNode.PROPERTY_CALL: return [this.nodes.node, nullSafe ? "?." : ".", this.nodes.attribute]; case GetAttrNode.METHOD_CALL: return [this.nodes.node, nullSafe ? "?." : ".", this.nodes.attribute, '(', this.nodes.fnArguments, ')']; case GetAttrNode.ARRAY_CALL: return [this.nodes.node, '[', this.nodes.attribute, ']']; } }); this.name = 'GetAttrNode'; } isShortCircuited() { return this.attributes.is_short_circuited || this.nodes.node instanceof GetAttrNode && this.nodes.node.isShortCircuited(); } } _defineProperty(GetAttrNode, "PROPERTY_CALL", 1); _defineProperty(GetAttrNode, "METHOD_CALL", 2); _defineProperty(GetAttrNode, "ARRAY_CALL", 3); class NullCoalesceNode extends Node { constructor(expr1, expr2) { super({ expr1: expr1, expr2: expr2 }); _defineProperty(this, "compile", compiler => { compiler.raw('((').compile(this.nodes.expr1).raw(") ?? (").compile(this.nodes.expr2).raw("))"); }); _defineProperty(this, "evaluate", (functions, values) => { if (this.nodes.expr1 instanceof GetAttrNode) { this._addNullCoalesceAttributeToGetAttrNodes(this.nodes.expr1); } return this.nodes.expr1.evaluate(functions, values) ?? this.nodes.expr2.evaluate(functions, values); }); _defineProperty(this, "toArray", () => { return ['(', this.nodes.expr1, ') ?? (', this.nodes.expr2, ')']; }); _defineProperty(this, "_addNullCoalesceAttributeToGetAttrNodes", node => { if (!node instanceof GetAttrNode) { return; } node.attributes.is_null_coalesce = true; for (let oneNode of Object.values(node.nodes)) { this._addNullCoalesceAttributeToGetAttrNodes(oneNode); } }); this.name = 'NullCoalesceNode'; } } class NullCoalescedNameNode extends Node { constructor(name) { super({}, { name }); _defineProperty(this, "compile", compiler => { compiler.raw(this.attributes.name + " ?? null"); }); _defineProperty(this, "evaluate", (functions, values) => { return null; }); _defineProperty(this, "toArray", () => { return [this.attributes.name + " ?? null"]; }); this.name = 'NullCoalescedNameNode'; } } const OPERATOR_LEFT = 1; const OPERATOR_RIGHT = 2; const IGNORE_UNKNOWN_VARIABLES = 1; const IGNORE_UNKNOWN_FUNCTIONS = 2; class Parser { constructor(functions = {}) { _defineProperty(this, "functions", {}); _defineProperty(this, "unaryOperators", { 'not': { 'precedence': 50 }, '!': { 'precedence': 50 }, '-': { 'precedence': 500 }, '+': { 'precedence': 500 }, '~': { 'precedence': 500 } }); _defineProperty(this, "binaryOperators", { 'or': { 'precedence': 10, 'associativity': OPERATOR_LEFT }, '||': { 'precedence': 10, 'associativity': OPERATOR_LEFT }, 'xor': { precedence: 12, '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': 25, '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, 'associati