@akala/core
Version:
717 lines • 28.2 kB
JavaScript
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