UNPKG

@andreasnicolaou/typescript-expression-language

Version:
1,502 lines (1,441 loc) 161 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.typescriptExpressionLanguage = {})); })(this, (function (exports) { 'use strict'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var addcslashes$1; var hasRequiredAddcslashes; function requireAddcslashes () { if (hasRequiredAddcslashes) return addcslashes$1; hasRequiredAddcslashes = 1; addcslashes$1 = 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 _pad(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 '\r': target += 'r'; break; case '\x07': 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; }; return addcslashes$1; } var addcslashesExports = requireAddcslashes(); var addcslashes = /*@__PURE__*/getDefaultExportFromCjs(addcslashesExports); var is_scalar$1; var hasRequiredIs_scalar; function requireIs_scalar () { if (hasRequiredIs_scalar) return is_scalar$1; hasRequiredIs_scalar = 1; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; is_scalar$1 = function is_scalar(mixedVar) { // 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 === "undefined" ? "undefined" : _typeof(mixedVar)) ); }; return is_scalar$1; } var is_scalarExports = requireIs_scalar(); var is_scalar = /*@__PURE__*/getDefaultExportFromCjs(is_scalarExports); /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Represents a node in an abstract syntax tree (AST) for an expression language. * @class Node * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class Node { nodes = Object.create(Object.prototype); attributes = Object.create(Object.prototype); constructor(nodes = Object.create(Object.prototype), attributes = Object.create(Object.prototype)) { this.nodes = nodes; this.attributes = attributes; } /** * Converts the Node instance to a string representation. * @returns The string representation of the Node. * @memberof Node */ toString() { const attributes = Object.keys(this.attributes).reduce((out, name) => { out.push(`${name}: '${this.attributes[name] ? this.attributes[name].toString().replace(/\n/g, '') : 'null'}'`); return out; }, []); const repr = [this.constructor.name + '(' + attributes.join(', ')]; if (Object.values(this.nodes).length > 0) { for (const node of Object.values(this.nodes)) { const lines = node.toString().split('\n'); for (const line of lines) { repr.push(' ' + line); } } repr.push(')'); } else { repr[0] += ')'; } return repr.join('\n'); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof Node */ compile(compiler) { for (const node of Object.values(this.nodes)) { node.compile(compiler); } } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The attribute value. * @memberof Node */ evaluate(functions, values) { const results = []; for (const node of Object.values(this.nodes)) { results.push(node.evaluate(functions, values)); } return results; } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof Node */ toArray() { throw new Error(`Dumping a "${this.constructor.name}" instance is not supported yet.`); } /** * Dumps the node as a string. * @returns The dumped string representation. * @memberof Node */ dump() { let dump = ''; for (const node of this.toArray()) { dump += is_scalar(node) ? node : node.dump(); } return dump; } /** * Escapes a string for use in dump output. * @param value - The string to escape. * @returns The escaped string. * @memberof Node */ dumpString(value) { return `"${addcslashes(value, '\0\t"\\')}"`; } /** * Determines whether an array is a hash (non-sequential keys). * @param value - The array to check. * @returns True if the array is a hash, false otherwise. * @memberof Node */ isHash(value) { let expectedKey = 0; for (const key of Object.keys(value)) { if (parseInt(key) !== expectedKey++) { return true; } } return false; } } /** * Represents a constant node in an abstract syntax tree (AST) for an expression language. * @class ConstantNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class ConstantNode extends Node { isNullSafe; isIdentifier; constructor(value, isIdentifier = false, isNullSafe = false) { super({}, { value }); this.isIdentifier = isIdentifier; this.isNullSafe = isNullSafe; } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof ConstantNode */ compile(compiler) { compiler.repr(this.attributes['value'], this.isIdentifier); } /** * Evaluates the node. * @param _functions - The available functions for evaluation. * @param _values - The current values for evaluation. * @returns The attribute value. * @memberof ConstantNode */ // eslint-disable-next-line @typescript-eslint/no-unused-vars evaluate(_functions, _values) { return this.attributes['value']; } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof ConstantNode */ toArray() { const array = []; const value = this.attributes['value']; if (this.isIdentifier) { array.push(value); } else if (value === true) { array.push('true'); } else if (value === false) { array.push('false'); } else if (value === null) { array.push('null'); } else if (typeof value === 'number' || typeof value === 'bigint') { array.push(value); } else if (typeof value === 'string') { array.push(this.dumpString(value)); } else if (typeof value === 'object' && !Array.isArray(value)) { array.push('{'); for (const [k, v] of Object.entries(value)) { array.push(new ConstantNode(k), ': ', new ConstantNode(v), ', '); } array.pop(); array.push('}'); } else if (Array.isArray(value)) { array.push('['); for (const v of value) { array.push(new ConstantNode(v), ', '); } array.pop(); array.push(']'); } return array; } } /** * Represents an array-like structure of an abstract syntax tree (AST) in the expression language. * @class ArrayNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class ArrayNode extends Node { index = -1; keyIndex = -1; arrayNodeType = 'Array'; constructor() { super(); } /** * Adds an element to the node. * @param value - The value node to add. * @param key - Optional key node. * @memberof ArrayNode */ addElement(value, key = null) { if (!key) { key = new ConstantNode(++this.index); } else { this.arrayNodeType = this.arrayNodeType === 'Array' ? 'Object' : 'Array'; } this.nodes[(++this.keyIndex).toString()] = key; this.nodes[(++this.keyIndex).toString()] = value; } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof ArrayNode */ compile(compiler) { const isObject = this.arrayNodeType === 'Object'; const openingBracket = isObject ? '{' : '['; const closingBracket = isObject ? '}' : ']'; compiler.raw(openingBracket); this.compileArguments(compiler, isObject); compiler.raw(closingBracket); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The evaluated value. * @memberof ArrayNode */ evaluate(functions, values) { const isArray = this.arrayNodeType === 'Array'; const result = isArray ? [] : {}; for (const pair of this.getKeyValuePairs()) { const evaluatedKey = isArray ? undefined : pair.key.evaluate(functions, values); const evaluatedValue = pair.value.evaluate(functions, values); if (isArray) { result.push(evaluatedValue); } else { result[evaluatedKey] = evaluatedValue; } } return result; } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof ArrayNode */ toArray() { const value = Object.create(Object.prototype); for (const pair of this.getKeyValuePairs()) { value[pair.key.attributes.value] = pair.value; } const array = []; if (this.isHash(value)) { array.push('{'); for (const [key, val] of Object.entries(value)) { array.push(new ConstantNode(key), ': ', val, ', '); } array.pop(); array.push('}'); } else { array.push('['); for (const val of Object.values(value)) { array.push(val, ', '); } array.pop(); array.push(']'); } return array; } /** * Retrieves key-value pairs from the node. * @returns An array of key-value pair objects. * @memberof ArrayNode */ getKeyValuePairs() { const pairs = []; const keys = Object.keys(this.nodes); for (let i = 0; i < keys.length; i += 2) { pairs.push({ key: this.nodes[keys[i]], value: this.nodes[keys[i + 1]] }); } return pairs; } /** * Compiles the node's arguments. * @param compiler - The Compiler instance. * @param withKeys - Whether to include keys in the compiled output. * @memberof ArrayNode */ compileArguments(compiler, withKeys = true) { for (const [index, pair] of this.getKeyValuePairs().entries()) { if (index > 0) { compiler.raw(', '); } if (withKeys) { compiler.compile(pair.key).raw(': '); } compiler.compile(pair.value); } } } /** * Represents a node for arguments of an abstract syntax tree (AST) in the expression language. * @class ArgumentsNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class ArgumentsNode extends ArrayNode { /** * Compiles the arguments * @param compiler - The Compiler instance to compile the node. * @memberof ArgumentsNode */ compile(compiler) { this.compileArguments(compiler, false); } /** * Converts the arguments node into an array representation. * @returns An array of the node's arguments. * @memberof ArgumentsNode */ toArray() { const array = []; this.getKeyValuePairs().forEach((pair, index, arr) => { array.push(pair.value); if (index < arr.length - 1) { array.push(', '); } }); return array; } } var levenshtein$1; var hasRequiredLevenshtein; function requireLevenshtein () { if (hasRequiredLevenshtein) return levenshtein$1; hasRequiredLevenshtein = 1; levenshtein$1 = function levenshtein(s1, s2, costIns, costRep, costDel) { // discuss at: https://locutus.io/php/levenshtein/ // original by: Carlos R. L. Rodrigues (https://www.jsfromhell.com) // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman) // revised by: Andrea Giammarchi (https://webreflection.blogspot.com) // reimplemented by: Brett Zamir (https://brett-zamir.me) // reimplemented by: Alexander M Beedie // reimplemented by: Rafał Kukawski (https://blog.kukawski.pl) // example 1: levenshtein('Kevin van Zonneveld', 'Kevin van Sommeveld') // returns 1: 3 // example 2: levenshtein("carrrot", "carrots") // returns 2: 2 // example 3: levenshtein("carrrot", "carrots", 2, 3, 4) // returns 3: 6 // var LEVENSHTEIN_MAX_LENGTH = 255 // PHP limits the function to max 255 character-long strings costIns = costIns == null ? 1 : +costIns; costRep = costRep == null ? 1 : +costRep; costDel = costDel == null ? 1 : +costDel; if (s1 === s2) { return 0; } var l1 = s1.length; var l2 = s2.length; if (l1 === 0) { return l2 * costIns; } if (l2 === 0) { return l1 * costDel; } // Enable the 3 lines below to set the same limits on string length as PHP does // if (l1 > LEVENSHTEIN_MAX_LENGTH || l2 > LEVENSHTEIN_MAX_LENGTH) { // return -1; // } var split = false; try { split = !'0'[0]; } catch (e) { // Earlier IE may not support access by string index split = true; } if (split) { s1 = s1.split(''); s2 = s2.split(''); } var p1 = new Array(l2 + 1); var p2 = new Array(l2 + 1); var i1 = void 0, i2 = void 0, c0 = void 0, c1 = void 0, c2 = void 0, tmp = void 0; for (i2 = 0; i2 <= l2; i2++) { p1[i2] = i2 * costIns; } for (i1 = 0; i1 < l1; i1++) { p2[0] = p1[0] + costDel; for (i2 = 0; i2 < l2; i2++) { c0 = p1[i2] + (s1[i1] === s2[i2] ? 0 : costRep); c1 = p1[i2 + 1] + costDel; if (c1 < c0) { c0 = c1; } c2 = p2[i2] + costIns; if (c2 < c0) { c0 = c2; } p2[i2 + 1] = c0; } tmp = p1; p1 = p2; p2 = tmp; } c0 = p1[l2]; return c0; }; return levenshtein$1; } var levenshteinExports = requireLevenshtein(); var levenshtein = /*@__PURE__*/getDefaultExportFromCjs(levenshteinExports); /** * Represents a syntax error in an expression. * @author Andreas Nicolaou <anicolaou66@gmail.com> */ let SyntaxError$1 = class SyntaxError extends Error { constructor(message, cursor = null, expression = '', subject = null, proposals = null) { const around = cursor != null ? ` around position ${cursor}` : ''; let formattedMessage = `${message.replace(/\.$/, '')}${around}`; if (expression) { formattedMessage = `${formattedMessage} for expression \`${expression}\``; } formattedMessage += '.'; if (subject !== null && proposals !== null) { let minScore = Infinity; let guess; for (const proposal of proposals) { const distance = levenshtein(subject, proposal); if (distance !== undefined && distance < minScore) { guess = proposal; minScore = distance; } } if (guess !== undefined && minScore < 3) { formattedMessage += ` Did you mean "${guess}"?`; } } super(formattedMessage); this.name = 'SyntaxError'; } }; /** * Represents a binary node in an abstract syntax tree (AST) for an expression language. * @class BinaryNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class BinaryNode extends Node { static OPERATORS = { '~': '.', and: '&&', or: '||', }; static FUNCTIONS = { '**': 'pow', '..': 'range', in: 'inArray', 'not in': 'notInArray', contains: 'strContains', 'starts with': 'strStartsWith', 'ends with': 'strEndsWith', }; constructor(operator, left, right) { super({ left, right }, { operator }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof BinaryNode */ compile(compiler) { const operator = this.attributes['operator']; if (operator === 'matches') { if (this.nodes.right instanceof ConstantNode) { this.evaluateMatches(this.nodes.right.evaluate({}, {}), ''); } else if (this.nodes.right instanceof BinaryNode && this.nodes.right.attributes['operator'] !== '~') { throw new SyntaxError$1('The regex passed to "matches" must be a string.'); } compiler .raw('(function (regexp, str) { try { if (regexp.startsWith("/") && regexp.endsWith("/")) { regexp = regexp.slice(1, -1); } return new RegExp(regexp).test(str ?? ""); } catch () { throw new SyntaxError(\'Invalid regex passed to "matches".\'); } })(') .compile(this.nodes.right) .raw(', ') .compile(this.nodes.left) .raw(')'); return; } if (operator in BinaryNode.FUNCTIONS) { const funcName = BinaryNode.FUNCTIONS[operator]; compiler.raw(`${funcName}(`).compile(this.nodes.left).raw(', ').compile(this.nodes.right).raw(')'); return; } compiler .raw('(') .compile(this.nodes.left) .raw(` ${BinaryNode.OPERATORS[operator] || operator} `) .compile(this.nodes.right) .raw(')'); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The evaluated value. * @memberof ArrayNode */ evaluate(functions, values) { const operator = this.attributes['operator']; const left = this.nodes.left?.evaluate(functions, values); let right = null; if (operator in BinaryNode.FUNCTIONS) { const right = this.nodes.right.evaluate(functions, values); switch (operator) { case 'in': return this.inArray(left, right); case 'not in': return this.notInArray(left, right); case '**': return this.pow(left, right); case '..': return this.range(left, right); case 'contains': return this.strContains(left, right); case 'starts with': return this.strStartsWith(left, right); case 'ends with': return this.strEndsWith(left, right); default: // This should never happen unless FUNCTIONS is modified incorrectly throw new Error(`Unsupported function operator: ${operator}`); } } 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 '<=': return left <= right; case '>=': return left >= right; case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': if (right === 0) { throw new Error('Division by zero.'); } return left / right; case '%': if (right === 0) { throw new Error('Modulo by zero.'); } return left % right; case 'matches': return this.evaluateMatches(right, left); case '~': return `${left}${right}`; default: throw new Error(`Operator "${operator}" not supported.`); } } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof BinaryNode */ toArray() { return ['(', this.nodes.left, ` ${this.attributes['operator']} `, this.nodes.right, ')']; } /** * Evaluates matches * @param regexp * @param str * @returns true if matches * @memberof BinaryNode */ evaluateMatches(regexp, str) { try { if (regexp.startsWith('/') && regexp.endsWith('/')) { // Remove the slashes and use the part in between as the pattern regexp = regexp.slice(1, -1); } return new RegExp(regexp).test(str ?? ''); } catch (error) { throw new SyntaxError$1(`Regexp "${regexp}" passed to "matches" is not valid.[${error}]`); } } pow(x, y) { return Math.pow(x, y); } range(start, end) { return Array.from({ length: end - start + 1 }, (_, i) => start + i); } inArray(item, array) { return array.indexOf(item) >= 0; } notInArray(item, array) { return array.indexOf(item) === -1; } strContains(str, substr) { return str.includes(substr); } strStartsWith(str, prefix) { return str.startsWith(prefix); } strEndsWith(str, suffix) { return str.endsWith(suffix); } } /** * Represents a conditional node in an abstract syntax tree (AST) for an expression language. * @class ConditionalNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class ConditionalNode extends Node { constructor(expr1, expr2, expr3) { super({ expr1, expr2, expr3 }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof ConditionalNode */ compile(compiler) { compiler .raw('((') .compile(this.nodes.expr1) .raw(') ? (') .compile(this.nodes.expr2) .raw(') : (') .compile(this.nodes.expr3) .raw('))'); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The evaluated value. * @memberof ConditionalNode */ evaluate(functions, values) { if (this.nodes.expr1.evaluate(functions, values)) { return this.nodes.expr2.evaluate(functions, values); } return this.nodes.expr3.evaluate(functions, values); } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof ConditionalNode */ toArray() { return ['(', this.nodes.expr1, ' ? ', this.nodes.expr2, ' : ', this.nodes.expr3, ')']; } } /** * Represents a function node in an abstract syntax tree (AST) for an expression language. * @class FunctionNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class FunctionNode extends Node { constructor(name, argumentsNode) { super({ arguments: argumentsNode }, { name }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof FunctionNode */ compile(compiler) { const args = []; for (const node of Object.values(this.nodes.arguments.nodes)) { args.push(compiler.subcompile(node)); } compiler.raw(compiler.getFunction(this.attributes.name)?.['compiler'](...args)); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The evaluated value. * @memberof FunctionNode */ evaluate(functions, values) { const args = [values]; for (const node of Object.values(this.nodes.arguments.nodes)) { args.push(node.evaluate(functions, values)); } return functions[this.attributes.name]['evaluator'](...args); } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof FunctionNode */ toArray() { const array = []; array.push(this.attributes.name); array.push('('); for (const node of Object.values(this.nodes.arguments.nodes)) { array.push(node, ', '); } array.pop(); array.push(')'); return array; } } /** * Represents a attribute node in an abstract syntax tree (AST) for an expression language. * @class GetAttrNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class GetAttrNode extends Node { static PROPERTY_CALL = 1; static METHOD_CALL = 2; static ARRAY_CALL = 3; constructor(node, attribute, argumentsNode, type) { super({ node, attribute, arguments: argumentsNode }, { type, is_null_coalesce: false, is_short_circuited: false }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof GetAttrNode */ 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.arguments) .raw(')'); break; case GetAttrNode.ARRAY_CALL: compiler.compile(this.nodes.node).raw('[').compile(this.nodes.attribute).raw(']'); break; } } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The evaluated value. * @memberof GetAttrNode */ evaluate(functions, values) { const valEvaluation = this.nodes.node.evaluate(functions, values); const property = this.nodes.attribute.attributes.value; switch (this.attributes.type) { case GetAttrNode.PROPERTY_CALL: if (valEvaluation === null && ((this.nodes.attribute instanceof ConstantNode && this.nodes.attribute.isNullSafe) || this.attributes.is_null_coalesce)) { this.attributes.is_short_circuited = true; return null; } if (valEvaluation === null && this.isShortCircuited()) { return null; } if (typeof valEvaluation !== 'object') { throw new Error(`Unable to get property "${this.nodes.attribute.dump()}" of non-object "${this.nodes.node.dump()}".`); } if (this.attributes.is_null_coalesce) { return valEvaluation[property] ?? null; } return valEvaluation[property]; case GetAttrNode.METHOD_CALL: if (valEvaluation === null && this.nodes.attribute instanceof ConstantNode && this.nodes.attribute.isNullSafe) { this.attributes.is_short_circuited = true; return null; } if (valEvaluation === null && this.isShortCircuited()) { return null; } if (typeof valEvaluation !== 'object') { throw new Error(`Unable to call method "${this.nodes.attribute.dump()}" of non-object "${this.nodes.node.dump()}".`); } if (typeof valEvaluation[property] !== 'function') { throw new Error(`Unable to call method "${property}" of object "${typeof valEvaluation}".`); } return valEvaluation[property](...Object.values(this.nodes['arguments'].evaluate(functions, values))); case GetAttrNode.ARRAY_CALL: if (valEvaluation === null && this.isShortCircuited()) { return null; } if (typeof valEvaluation !== 'object' && !(valEvaluation instanceof Array)) { throw new Error(`Unable to get an item of non-array "${this.nodes.node.dump()}".`); } if (this.attributes.is_null_coalesce) { return valEvaluation[this.nodes.attribute.evaluate(functions, values)] ?? null; } return valEvaluation[this.nodes.attribute.evaluate(functions, values)]; } } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof GetAttrNode */ 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.arguments, ')']; case GetAttrNode.ARRAY_CALL: return [this.nodes.node, '[', this.nodes.attribute, ']']; default: return []; } } /** * Determines short circuited * @returns true if short circuited * @memberof GetAttrNode */ isShortCircuited() { return (this.attributes.is_short_circuited || (this.nodes.node instanceof GetAttrNode && this.nodes.node.isShortCircuited())); } } /** * Represents a name node in an abstract syntax tree (AST) for an expression language. * @class NameNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class NameNode extends Node { constructor(name) { super({}, { name }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof NameNode */ compile(compile) { compile.raw(`${this.attributes.name}`); } /** * Evaluates the node. * @param _functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The attribute value. * @memberof NameNode */ evaluate(_functions, values) { return values[this.attributes.name]; } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof NameNode */ toArray() { return [this.attributes.name]; } } /** * Represents a null-coalesce in an abstract syntax tree (AST) for an expression language. * @class NullCoalesceNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class NullCoalesceNode extends Node { constructor(expr1, expr2) { super({ expr1, expr2 }, {}); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof NullCoalesceNode */ compile(compiler) { compiler.raw('((').compile(this.nodes.expr1).raw(') ?? (').compile(this.nodes.expr2).raw('))'); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The attribute value. * @memberof NullCoalesceNode */ 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); } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof NullCoalesceNode */ toArray() { return ['(', this.nodes.expr1, ') ?? (', this.nodes.expr2, ')']; } /** * Adds null coalesce attribute to get attr nodes * @param node * @returns null coalesce attribute to get attr nodes * @memberof NullCoalesceNode */ addNullCoalesceAttributeToGetAttrNodes(node) { if (!(node instanceof GetAttrNode)) { return; } node.attributes['is_null_coalesce'] = true; for (const n of Object.values(node.nodes)) { this.addNullCoalesceAttributeToGetAttrNodes(n); } } } /** * Represents a null-coalesced-name in an abstract syntax tree (AST) for an expression language. * @class NullCoalescedNameNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class NullCoalescedNameNode extends Node { constructor(name) { super({}, { name }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof NullCoalescedNameNode */ compile(compiler) { compiler.raw(`${this.attributes.name} ?? null`); } /** * Evaluates the node. * @param _functions - The available functions for evaluation. * @param _values - The current values for evaluation. * @returns null. * @memberof NullCoalescedNameNode */ // eslint-disable-next-line @typescript-eslint/no-unused-vars evaluate(_functions, _values) { return null; } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof NullCoalescedNameNode */ toArray() { return [`${this.attributes.name} ?? null`]; } } /** * Represents a unary node in an abstract syntax tree (AST) for an expression language. * @class UnaryNode * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class UnaryNode extends Node { static OPERATORS = { '!': '!', not: '!', '+': '+', '-': '-', '~': '~', }; constructor(operator, node) { super({ node }, { operator }); } /** * Compiles the node. * @param compiler - The Compiler instance. * @memberof UnaryNode */ compile(compiler) { compiler.raw('(').raw(UnaryNode.OPERATORS[this.attributes.operator]).compile(this.nodes.node).raw(')'); } /** * Evaluates the node. * @param functions - The available functions for evaluation. * @param values - The current values for evaluation. * @returns The attribute value. * @memberof UnaryNode */ evaluate(functions, values) { const value = this.nodes.node.evaluate(functions, values); switch (this.attributes.operator) { case 'not': case '!': return !value; case '-': return -value; case '~': return ~value; default: return value; } } /** * Converts the node to an array representation. * @returns The array representation of the node. * @memberof UnaryNode */ toArray() { return ['(', `${this.attributes.operator} `, this.nodes.node, ')']; } } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Represents a compiler for an expression language. * @class Compiler * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class Compiler { functions; source = ''; constructor(functions) { this.functions = functions; } /** * Gets function * @param name * @returns function * @memberof Compiler */ getFunction(name) { return this.functions[name]; } /** * Gets the current source code after compilation. * @returns string * @memberof Compiler */ getSource() { return this.source; } /** * Resets the state of the compiler. * @returns this * @memberof Compiler */ reset() { this.source = ''; return this; } /** * Compiles a node. * @returns this * @memberof Compiler */ compile(node) { node.compile(this); return this; } subcompile(node) { const current = this.source; this.source = ''; node.compile(this); const result = this.source; this.source = current; return result; } /** * Adds a raw string to the compiled code. * @returns this * @memberof Compiler */ raw(string) { this.source += string; return this; } /** * Adds a quoted string to the compiled code. * @returns this * @memberof Compiler */ string(value) { this.source += `"${addcslashes(value ?? '', '\0\t"\\')}"`; return this; } /** * Returns a representation of a given value. * @returns this * @memberof Compiler */ repr(value, isIdentifier = false) { // Check if the value is a number before performing arithmetic operations if (isIdentifier) { this.raw(value); } else if (Number.isInteger(value) || (+value === value && (!isFinite(value) || !!(value % 1)))) { this.raw('' + value); } else if (value === null) { this.raw('null'); } else if (typeof value === 'boolean') { this.raw(value ? 'true' : 'false'); } else if (typeof value === 'object' && !Array.isArray(value)) { this.raw('{'); let first = true; for (const oneKey of Object.keys(value)) { if (!first) { this.raw(', '); } first = false; this.repr(oneKey); this.raw(':'); this.repr(value[oneKey]); } this.raw('}'); } else if (Array.isArray(value)) { this.raw('['); let first = true; for (const val of value) { if (!first) { this.raw(', '); } first = false; this.repr(val); } this.raw(']'); } else { // If not number, boolean, object, or array, treat it as a string this.string(value); } return this; } } /** * Represents an expression. * @class Expression * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class Expression { expression; constructor(expression) { this.expression = expression; } /** * Gets the expression. * @returns The string representation of the expression * @memberof Expression */ toString() { return this.expression; } } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Represents an expression function in an expression language. * @class ExpressionFunction * @author Andreas Nicolaou <anicolaou66@gmail.com> */ class ExpressionFunction { name; compiler; evaluator; constructor(name, compiler, evaluator) { this.name = name; this.compiler = compiler; this.evaluator = evaluator; } /** * Creates an ExpressionFunction from a JavaScript function name. * @param jsFunctionName The JavaScript function name * @param expressionFunctionName The expression function name (optional) * @throws Error if the JavaScript function does not exist. * @memberof ExpressionFunction */ static fromJs(jsFunctionName, customFunction, expressionFunctionName) { const func = customFunction || ExpressionFunction.resolveJsFunction(jsFunctionName); if (typeof func !== 'function') { throw new Error(`JavaScript function "${jsFunctionName}" does not exist.`); } const compiler = (...args) => { const formattedArgs = args.map((arg) => { if (arg instanceof RegExp) { return arg.toString(); } if (typeof arg === 'object' && arg !== null) {