UNPKG

@akala/core

Version:
717 lines 28.2 kB
import { UnaryOperator } from './expressions/unary-operator.js'; import { BinaryOperator } from './expressions/binary-operator.js'; import { TernaryOperator } from './expressions/ternary-operator.js'; import { ExpressionType } from './expressions/expression-type.js'; import { Expression } from './expressions/expression.js'; import { NewExpression } from './expressions/new-expression.js'; import { ConstantExpression } from './expressions/constant-expression.js'; import { MemberExpression } from './expressions/member-expression.js'; import { UnaryExpression } from './expressions/unary-expression.js'; import { BinaryExpression } from './expressions/binary-expression.js'; import { TernaryExpression } from './expressions/ternary-expression.js'; import { CallExpression } from './expressions/call-expression.js'; import identity from '../formatters/identity.js'; import negate from '../formatters/negate.js'; import booleanize from '../formatters/booleanize.js'; import { formatters } from '../formatters/index.js'; import { escapeRegExp } from '../reflect.js'; import { AssignmentOperator } from './expressions/assignment-operator.js'; import { AssignmentExpression } from './expressions/assignment-expression.js'; import ErrorWithStatus, { HttpStatusCode } from '../errorWithStatus.js'; const jsonKeyRegex = /\s*(?:(?:"([^"]+)")|(?:'([^']+)')|(?:([a-zA-Z0-9_$]+)) *):\s*/; export class StringCursor { string; /** * Gets the length of the string being parsed. */ get length() { return this.string.length; } ; /** * Gets the current character at the cursor position. */ get char() { return this.string[this._offset]; } ; /** * Gets whether the cursor has reached the end of the file. */ get eof() { return this._offset >= this.string.length; } ; /** * Creates a new StringCursor instance. * @param string - The string to parse. */ constructor(string) { this.string = string; } /** * Gets the current line number based on the cursor position. * @returns The line number. */ getLineNo() { let n = 0; for (let i = 0; i <= this.offset; i++) { if (this.string[i] === '\n') n++; } return n; } getLine() { const nextLine = this.string.indexOf('\n', this.offset); if (nextLine == -1) return this.string.substring(this._offset - this.getColumn()); else return this.string.substring(this._offset - this.getColumn(), nextLine); } /** * Gets the current column position within the current line. * @returns The column number (0-based). */ getColumn() { for (let i = this.offset; i >= 0; i--) { if (this.string[i] == '\n') return this.offset - i; } } /** * Gets a human-readable representation of the current cursor position. * @returns A string in the format "line:column". */ getReadableOffset() { return this.getLineNo() + ':' + this.getColumn(); } _offset = 0; /** * Gets the current cursor offset in the string. */ get offset() { return this._offset; } ; /** * Sets the cursor offset in the string. * @throws Error if the offset is beyond the string length. */ set offset(value) { this._offset = value; if (this._offset > this.string.length) throw new Error('Cursor cannot go beyond the string limit'); } ; /** * Creates a copy of the current cursor state. * @returns A new StringCursor with the same position and content. */ freeze() { const c = new StringCursor(this.string); c._offset = this._offset; return c; } /** * Executes a regular expression at the current cursor position. * @param regex - The regular expression to execute. * @returns The regex match result or null if no match at current position. */ exec(regex) { if (!regex.global) regex = new RegExp(regex, 'g' + regex.flags); regex.lastIndex = this._offset; const result = regex.exec(this.string); if (result) { if (result.index != this._offset) return null; this.offset += result[0].length; } return result; } /** * Reads a specific string at the current cursor position. * @param s - The string to read. * @returns true if the string was found and consumed, false otherwise. */ read(s) { if (this.string.length < this._offset + s.length) return false; for (let i = 0; i < s.length; i++) { if (s.charCodeAt(i) !== this.string.charCodeAt(this._offset + i)) return false; } this._offset += s.length; return true; } /** * Reads a specific string, skipping whitespace before and after. * @param s - The string to read. * @returns true if the string was found and consumed, false otherwise. */ trimRead(s) { this.skipWhitespace(); if (this.read(s)) { this.skipWhitespace(); return true; } return false; } /** * Reads a specific string, skipping whitespace before the string. * @param s - The string to read. * @returns true if the string was found and consumed, false otherwise. */ trimStartRead(s) { this.skipWhitespace(); return this.read(s); } /** * Reads a specific string, skipping whitespace after the string. * @param s - The string to read. * @returns true if the string was found and consumed, false otherwise. */ trimEndRead(s) { if (this.read(s)) { this.skipWhitespace(); return true; } return false; } /** * Skips any whitespace characters at the current cursor position. * @returns The skipped whitespace string or undefined if none found. */ skipWhitespace() { return this.exec(/\s+/)?.[0]; } } /** * @deprecated Please use ObservableObject.setValue instead which more versatile * Gets the setter function for a given expression and root object. * @param {string} expression - The expression to evaluate. * @param {T} root - The root object. * @returns {{ expression: string, target: T, set: (value: unknown) => void } | null} The setter function or null if not found. */ export function getSetter(expression, root) { let target = root; const parts = expression.split('.'); while (parts.length > 1 && typeof (target) != 'undefined') { target = this.eval(parts[0], target); parts.shift(); } if (typeof (target) == 'undefined') return null; return { expression: parts[0], target: target, set: function (value) { target[parts[0]] = value; } }; } /** * Parses a binary operator from a string. * @param {string} op - The operator string. * @returns {BinaryOperator} The parsed binary operator. */ function parseBinaryOperator(op) { if (op in BinaryOperator) return op; return BinaryOperator.Unknown; } function parseAssignmentOperator(op) { if (op in AssignmentOperator) return op; return AssignmentOperator.Unknown; } /** * Parses a ternary operator from a string. * @param {string} op - The operator string. * @returns {TernaryOperator} The parsed ternary operator. */ function parseTernaryOperator(op) { switch (op) { case '?': return TernaryOperator.Question; default: return TernaryOperator.Unknown; } } /** * Represents a format expression. * @extends Expression */ export class FormatExpression extends Expression { lhs; formatter; settings; constructor(lhs, formatter, settings) { super(); this.lhs = lhs; this.formatter = formatter; this.settings = settings; } get type() { return ExpressionType.Format; } accept(visitor) { return visitor.visitFormat(this); } } /** * Represents a parsed object. * @extends NewExpression */ export class ParsedObject extends NewExpression { constructor(...init) { super(...init); } } /** * Represents a parsed array. * @extends NewExpression */ export class ParsedArray extends NewExpression { constructor(...init) { super(...init); this.newType = '['; } } /** * Represents a parsed string. * @extends ConstantExpression */ export class ParsedString extends ConstantExpression { constructor(value) { super(value); } toString() { return this.value; } } /** * Represents a parsed number. * @extends ConstantExpression */ export class ParsedNumber extends ConstantExpression { constructor(value) { super(Number(value)); } } /** * Represents a parsed boolean. * @extends ConstantExpression */ export class ParsedBoolean extends ConstantExpression { constructor(value) { super(Boolean(value)); } } /** * Represents a parser. */ export class Parser { static parameterLess = new Parser(); parameters; constructor(...parameters) { if (parameters) { this.parameters = {}; parameters.forEach(param => { this.parameters[param.name] = param; }); } } /** * Parses an expression. * @param {string} expression - The expression to parse. * @param {boolean} [parseFormatter=true] - Whether to parse formatters. * @param {() => void} [reset] - The reset function. * @returns {Expressions} The parsed expression. */ parse(expression, parseFormatter, reset) { expression = expression.trim(); const cursor = new StringCursor(expression); const result = this.parseAny(cursor, (typeof parseFormatter !== 'boolean') || parseFormatter, reset); if (cursor.offset < cursor.length) throw new Error(`invalid character ${cursor.char} at ${cursor.offset}`); return result; } /** * Parses any expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {() => void} [reset] - The reset function. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed expression. */ parseAny(expression, parseFormatter, reset) { switch (expression.char) { case '{': return this.parseObject(expression, parseFormatter); case '[': return this.parseArray(expression, parseFormatter, reset); case '"': case "'": return this.parseString(expression, expression.char, parseFormatter); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': return this.parseNumber(expression, parseFormatter); default: return this.parseEval(expression, parseFormatter, reset); } } /** * Parses a number expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed number expression. */ parseNumber(expression, parseFormatter) { const match = expression.exec(/\d+(?:\.\d+)?/); if (!match) throw new Error('Invalid number at position ' + expression.offset); const result = new ParsedNumber(match[0]); return this.tryParseOperator(expression, result, parseFormatter); } /** * Parses a boolean expression. * @param {string} expression - The expression to parse. * @returns {ParsedBoolean} The parsed boolean expression. */ parseBoolean(expression) { let formatter = identity; if (expression.char == '!') { formatter = negate; expression.offset++; } if (expression.char == '!') { formatter = booleanize; expression.offset++; } const boolMatch = expression.exec(/(?:true|false|undefined|null)/); if (boolMatch) { const result = new ParsedBoolean(boolMatch[0]); if (formatter !== identity) { return this.tryParseOperator(expression, new ParsedBoolean(formatter.instance.format(result.value)), true); } return this.tryParseOperator(expression, result, true); } else if (formatter !== identity) { return new FormatExpression(this.parseAny(expression, true), formatter, null); } return null; } /** * Parses an evaluation expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {() => void} [reset] - The reset function. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed evaluation expression. */ parseEval(expression, parseFormatter, reset) { const b = this.parseBoolean(expression); if (b) return b; return this.parseFunction(expression, parseFormatter, reset); } /** * Parses a function expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {() => void} [reset] - The reset function. * @returns {Expressions} The parsed function expression. */ parseFunction(expression, parseFormatter, reset) { let operator; while (expression.char === '!') { if (expression.char === '!') { operator = UnaryOperator.Not; expression.offset++; } if (expression[0] === '!') { operator = UnaryOperator.NotNot; expression.offset++; } } let item = expression.exec(/[\w$]*\??/)[0]; const optional = item.endsWith('?'); if (optional) item = item.substring(0, item.length - 1); if (item) { let result; if (this.parameters) // eslint-disable-next-line @typescript-eslint/no-explicit-any result = new MemberExpression(this.parameters[''], new ParsedString(item), optional); else result = new MemberExpression(null, new ParsedString(item), optional); if (typeof operator != 'undefined') { result = new UnaryExpression(result, operator); } return this.tryParseOperator(expression, result, parseFormatter, reset); } return this.tryParseOperator(expression, null, parseFormatter, reset); } /** * Parses a formatter expression. * @param {string} expression - The expression to parse. * @param {Expressions} lhs - The left-hand side expression. * @param {() => void} reset - The reset function. * @returns {Expressions} The parsed formatter expression. */ parseFormatter(expression, lhs, reset) { const item = expression.exec(/\s*([\w.$]+)\s*/); reset?.(); let settings; if (expression.char === ':') { expression.offset++; settings = this.parseAny(expression, false); } const result = new FormatExpression(lhs, formatters.resolve(item[1]), settings); return this.tryParseOperator(expression, result, true, reset); } /** * Tries to parse an operator expression. * @param {string} expression - The expression to parse. * @param {Expressions} lhs - The left-hand side expression. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {() => void} [reset] - The reset function. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed operator expression. */ tryParseOperator(expression, lhs, parseFormatter, reset) { const operator = expression.exec(/\s*([<>=!+\-/*&|?.#[(]+)\s*/); if (operator) { let rhs; const oldParameters = this.parameters; let binaryOperator = parseBinaryOperator(operator[1]); let assignmentOperator = parseAssignmentOperator(operator[1]); const ternaryOperator = parseTernaryOperator(operator[1]); let group = 0; switch (operator[1]) { case '#': if (!parseFormatter) { expression.offset -= operator[0].length; return lhs; } return this.parseFormatter(expression, lhs, reset); case '(': case '.(': reset?.(); if (lhs) { expression.offset--; return this.parseFunctionCall(expression, lhs, parseFormatter); } else { lhs = this.parseAny(expression, parseFormatter, reset); expression.offset++; return this.tryParseOperator(expression, lhs, parseFormatter, reset); } case '[': { rhs = this.parseAny(expression, parseFormatter, reset); // eslint-disable-next-line @typescript-eslint/no-explicit-any const member = new MemberExpression(lhs, rhs, false); expression.offset++; // Skip closing bracket return this.tryParseOperator(expression, member, parseFormatter, reset); } case '?.': case '.': { this.parameters = { ...this.parameters, '': lhs }; const selfReset = (() => { this.parameters = oldParameters; }); rhs = this.parseAny(expression, parseFormatter, reset || selfReset); selfReset(); return rhs; } case '?': { const second = this.parseAny(expression, parseFormatter, reset); const operator2 = expression.exec(/\s*(:)\s*/); if (!operator2) throw new Error('Invalid ternary operator'); const third = this.parseAny(expression, parseFormatter, reset); const ternary = new TernaryExpression(lhs, ternaryOperator, second, third); return ternary; } default: { reset?.(); if (ternaryOperator == TernaryOperator.Unknown && assignmentOperator == AssignmentOperator.Unknown && binaryOperator == BinaryOperator.Unknown) { while (operator[1][operator[1].length - 1] == '(') { group++; operator[1] = operator[1].substring(0, operator[1].length - 1); } binaryOperator = parseBinaryOperator(operator[1]); assignmentOperator = parseAssignmentOperator(operator[1]); if (assignmentOperator == AssignmentOperator.Unknown && binaryOperator == BinaryOperator.Unknown) { throw new ErrorWithStatus(HttpStatusCode.BadRequest, `Invalid expression at offset ${expression.offset} (${expression.char})`); } } rhs = this.parseAny(expression, parseFormatter); expression.offset += group; if (binaryOperator !== BinaryOperator.Unknown) { const binary = new BinaryExpression(lhs, binaryOperator, rhs); if (group == 0) return BinaryExpression.applyPrecedence(binary); return this.tryParseOperator(expression, binary, parseFormatter, reset); } if (assignmentOperator !== AssignmentOperator.Unknown) return new AssignmentExpression(lhs, assignmentOperator, rhs); } } } return lhs; } /** * Parses a function call expression. * @param {string} expression - The expression to parse. * @param {Expressions} lhs - The left-hand side expression. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed function call expression. */ parseFunctionCall(expression, lhs, parseFormatter) { const results = []; const optional = expression.offset > 1 && expression.string[expression.offset - 2] == '?'; this.parseCSV(expression, () => { const item = this.parseAny(expression, parseFormatter); results.push(item); return item; }, ')'); if (lhs?.type == ExpressionType.MemberExpression) return this.tryParseOperator(expression, new CallExpression( // eslint-disable-next-line @typescript-eslint/no-explicit-any lhs.source, lhs.member, results, optional), parseFormatter); return this.tryParseOperator(expression, new CallExpression(lhs, null, results, optional), parseFormatter); } /** * Parses an array expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {StringCursor} cursor - The cursor tracking the current position. * @param {() => void} [reset] - The reset function. * @returns {Expressions} The parsed array expression. */ parseArray(expression, parseFormatter, reset) { const results = []; this.parseCSV(expression, () => { const item = this.parseAny(expression, true); results.push(item); return item; }, ']'); //eslint-disable-next-line @typescript-eslint/no-explicit-any return this.tryParseOperator(expression, new ParsedArray(...results.map((v, i) => new MemberExpression(v, new ParsedNumber(i.toString()), false))), parseFormatter, reset); } /** * Parses a string expression. * @param {string} expression - The expression to parse. * @param {string} start - The starting character of the string. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed string expression. */ parseString(expression, start, parseFormatter) { start = escapeRegExp(start); const evaluatedRegex = expression.exec(new RegExp(start + "((?:[^\\" + start + "]|\\.)*)" + start)); if (!evaluatedRegex) throw new Error('Invalid string at position ' + expression.offset); const result = evaluatedRegex[1]; const parsedString = new ParsedString(result); return this.tryParseOperator(expression, parsedString, parseFormatter); } /** * Operates on two values using a binary operator. * @param {BinaryOperator} operator - The binary operator. * @param {unknown} [left] - The left-hand side value. * @param {unknown} [right] - The right-hand side value. * @returns {unknown} The result of the operation. */ static operate(operator, left, right) { switch (operator) { case BinaryOperator.Equal: return left == right; case BinaryOperator.StrictEqual: return left === right; case BinaryOperator.LessThan: return left < right; case BinaryOperator.LessThanOrEqual: return left <= right; case BinaryOperator.GreaterThan: return left > right; case BinaryOperator.GreaterThanOrEqual: return left >= right; case BinaryOperator.NotEqual: return left != right; case BinaryOperator.StrictNotEqual: return left !== right; case BinaryOperator.Plus: return left + right; case BinaryOperator.Minus: return left - right; case BinaryOperator.Div: return left / right; case BinaryOperator.Times: return left * right; case BinaryOperator.Or: return left || right; case BinaryOperator.And: return left && right; case BinaryOperator.Dot: if (right instanceof Function) return right(left); return left[right]; default: throw new Error('invalid operator' + operator); } } /** * Parses a CSV expression. * @param {string} expression - The expression to parse. * @param {(expression: string) => Expressions} parseItem - The function to parse each item. * @param {string} end - The ending character of the CSV. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {number} The length of the parsed CSV expression. */ parseCSV(expression, parseItem, end) { const startOffset = expression.offset; expression.offset++; // Skip opening character for (; expression.offset < expression.length && expression.char !== end; expression.offset++) { // Skip whitespace while ((expression.char === ' ' || expression.char == '\n' || expression.char == '\t') && expression.offset < expression.length) expression.offset++; if (expression.offset >= expression.length) break; if (expression.char === end) { expression.offset++; break; } parseItem(); // Remove unused assignment // Skip whitespace while (expression.char === ' ' || expression.char == '\n' || expression.char == '\t') expression.offset++; // Check for comma if (expression.char === ',') continue; // If no comma, must be end character if (expression.char === end) break; throw new Error(`Expected comma or ${end} at position ${expression.offset}, but found ${expression.char}`); } expression.offset++; return expression.offset - startOffset; } /** * Parses an object expression. * @param {string} expression - The expression to parse. * @param {boolean} parseFormatter - Whether to parse formatters. * @param {StringCursor} cursor - The cursor tracking the current position. * @returns {Expressions} The parsed object expression. */ parseObject(expression, parseFormatter) { const parsedObject = []; this.parseCSV(expression, () => { const keyMatch = expression.exec(jsonKeyRegex); const key = keyMatch[1] || keyMatch[2] || keyMatch[3]; const item = this.parseAny(expression, true); parsedObject.push({ key, value: item }); // console.log(expression); //console.log(length); return item; }, '}'); //eslint-disable-next-line @typescript-eslint/no-explicit-any return this.tryParseOperator(expression, new ParsedObject(...parsedObject.map(v => new MemberExpression(v.value, new ParsedString(v.key), false))), parseFormatter); } } //# sourceMappingURL=parser.js.map