@andreasnicolaou/typescript-expression-language
Version:
TypeScript implementation of symfony/expression-language
1,502 lines (1,441 loc) • 161 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.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) {