@pawel-up/jexl
Version:
Javascript Expression Language: Powerful context-based expression parser and evaluator
360 lines • 11.2 kB
JavaScript
import { states } from './states.js';
export default class Parser {
_grammar;
_state;
_tree;
_exprStr;
_relative;
_stopMap;
_subParser;
_parentStop;
_cursor;
_nextIdentEncapsulate;
_nextIdentRelative;
_curObjKey;
constructor(grammar, prefix, stopMap = {}) {
this._grammar = grammar;
this._state = 'expectOperand';
this._tree = null;
this._exprStr = prefix || '';
this._relative = false;
this._stopMap = stopMap;
}
addToken(token) {
if (this._state === 'complete') {
throw new Error('Cannot add a new token to a completed Parser');
}
const state = states[this._state];
if (!state) {
throw new Error(`Invalid parser state: ${this._state}`);
}
const startExpr = this._exprStr;
this._exprStr += token.raw;
if (state.subHandler) {
if (!this._subParser) {
this._startSubExpression(startExpr);
}
const stopState = this._subParser.addToken(token);
if (stopState) {
this._endSubExpression();
if (this._parentStop)
return stopState;
this._state = stopState;
}
}
else if (state.tokenTypes && state.tokenTypes[token.type]) {
const typeOpts = state.tokenTypes[token.type];
if (!typeOpts) {
throw new Error(`No type options for token ${token.type}`);
}
if (typeOpts.handler) {
const handlerMethod = this._getTokenHandlerMethod(typeOpts.handler);
if (handlerMethod) {
handlerMethod(token);
}
}
else {
const handlerMethod = this._getHandlerMethod(token.type);
if (handlerMethod) {
handlerMethod(token);
}
}
if (typeOpts.toState) {
this._state = typeOpts.toState;
}
}
else if (this._stopMap[token.type]) {
return this._stopMap[token.type];
}
else {
throw new Error(`Token ${token.raw} (${token.type}) unexpected in expression: ${this._exprStr}`);
}
return false;
}
addTokens(tokens) {
tokens.forEach(this.addToken, this);
}
complete() {
const currentState = states[this._state];
if (this._cursor && (!currentState || !currentState.completable)) {
throw new Error(`Unexpected end of expression: ${this._exprStr}`);
}
if (this._subParser) {
this._endSubExpression();
}
this._state = 'complete';
return this._cursor ? this._tree : null;
}
isRelative() {
return this._relative;
}
_endSubExpression() {
const currentState = states[this._state];
if (!currentState || !currentState.subHandler) {
throw new Error(`Invalid state for ending sub expression: ${this._state}`);
}
const subHandlerName = currentState.subHandler;
const handlerMethod = this._getSubHandlerMethod(subHandlerName);
if (handlerMethod) {
handlerMethod(this._subParser.complete());
}
this._subParser = null;
}
_placeAtCursor(node) {
if (!this._cursor) {
this._tree = node;
}
else {
this._cursor.right = node;
this._setParent(node, this._cursor);
}
this._cursor = node;
}
_placeBeforeCursor(node) {
this._cursor = this._cursor?._parent;
this._placeAtCursor(node);
}
_setParent(node, parent) {
Object.defineProperty(node, '_parent', {
value: parent,
writable: true,
});
}
_startSubExpression(exprStr) {
let endStates = states[this._state].endStates;
if (!endStates) {
this._parentStop = true;
endStates = this._stopMap;
}
this._subParser = new Parser(this._grammar, exprStr, endStates);
}
argVal(ast) {
if (ast) {
this._cursor?.args?.push(ast);
}
}
arrayStart() {
this._placeAtCursor({
type: 'ArrayLiteral',
value: [],
});
}
arrayVal(ast) {
const { _cursor } = this;
if (ast && _cursor && Array.isArray(_cursor.value)) {
_cursor.value.push(ast);
}
}
binaryOp(token) {
const precedence = this._grammar.elements[token.value]?.precedence || 0;
let parent = this._cursor?._parent;
while (parent && parent.operator && this._grammar.elements[parent.operator]?.precedence >= precedence) {
this._cursor = parent;
parent = parent._parent;
}
const node = {
type: 'BinaryExpression',
operator: token.value,
left: this._cursor,
};
if (this._cursor) {
this._setParent(this._cursor, node);
}
this._cursor = parent;
this._placeAtCursor(node);
}
dot() {
this._nextIdentEncapsulate = Boolean(this._cursor &&
this._cursor.type !== 'UnaryExpression' &&
(this._cursor.type !== 'BinaryExpression' || (this._cursor.type === 'BinaryExpression' && this._cursor.right)));
this._nextIdentRelative = !this._cursor || (this._cursor && !this._nextIdentEncapsulate);
if (this._nextIdentRelative) {
this._relative = true;
}
}
filter(ast) {
this._placeBeforeCursor({
type: 'FilterExpression',
expr: ast,
relative: this._subParser.isRelative(),
subject: this._cursor,
});
}
functionCall() {
if (this._cursor && this._cursor.type === 'FunctionCall' && this._cursor.pool === 'transforms') {
return;
}
const functionName = this._buildFullIdentifierPath(this._cursor || null);
this._placeBeforeCursor({
type: 'FunctionCall',
name: functionName,
args: [],
pool: 'functions',
});
}
_buildFullIdentifierPath(node) {
if (!node || node.type !== 'Identifier') {
return node?.value || '';
}
const parts = [];
let current = node;
while (current && current.type === 'Identifier') {
parts.unshift(current.value);
current = current.from || null;
}
return parts.join('.');
}
identifier(token) {
const node = {
type: 'Identifier',
value: token.value,
};
if (this._nextIdentEncapsulate &&
this._cursor &&
this._cursor.type === 'FunctionCall' &&
this._cursor.pool === 'transforms') {
const namespaceParts = [];
namespaceParts.push(this._cursor.name);
namespaceParts.push(token.value);
const namespacedTransformName = namespaceParts.join('.');
this._cursor.name = namespacedTransformName;
this._nextIdentEncapsulate = false;
return;
}
if (this._nextIdentEncapsulate) {
node.from = this._cursor;
this._placeBeforeCursor(node);
this._nextIdentEncapsulate = false;
}
else {
if (this._nextIdentRelative) {
node.relative = true;
this._nextIdentRelative = false;
}
this._placeAtCursor(node);
}
}
literal(token) {
this._placeAtCursor({
type: 'Literal',
value: token.value,
});
}
objKey(token) {
this._curObjKey = token.value;
}
objStart() {
this._placeAtCursor({
type: 'ObjectLiteral',
value: {},
});
}
objVal(ast) {
if (this._cursor && this._curObjKey) {
;
this._cursor.value[this._curObjKey] = ast;
}
}
subExpression(ast) {
this._placeAtCursor(ast);
}
ternaryEnd(ast) {
if (this._cursor) {
this._cursor.alternate = ast;
}
}
ternaryMid(ast) {
if (this._cursor) {
this._cursor.consequent = ast;
}
}
ternaryStart() {
this._tree = {
type: 'ConditionalExpression',
test: this._tree || undefined,
};
this._cursor = this._tree;
}
transform(token) {
const transformName = token.value;
this._placeBeforeCursor({
type: 'FunctionCall',
name: transformName,
args: this._cursor ? [this._cursor] : [],
pool: 'transforms',
});
}
unaryOp(token) {
this._placeAtCursor({
type: 'UnaryExpression',
operator: token.value,
});
}
_getHandlerMethod(tokenType) {
switch (tokenType) {
case 'binaryOp':
return this.binaryOp.bind(this);
case 'dot':
return () => this.dot();
case 'identifier':
return this.identifier.bind(this);
case 'literal':
return this.literal.bind(this);
case 'unaryOp':
return this.unaryOp.bind(this);
case 'pipe':
return () => this.pipe();
default:
return undefined;
}
}
pipe() {
if (this._state === 'traverse') {
this._placeAtCursor({
type: 'Identifier',
value: '.',
relative: true,
});
this._relative = true;
}
}
_getTokenHandlerMethod(handlerName) {
switch (handlerName) {
case 'arrayStart':
return () => this.arrayStart();
case 'functionCall':
return () => this.functionCall();
case 'objKey':
return this.objKey.bind(this);
case 'objStart':
return () => this.objStart();
case 'ternaryStart':
return () => this.ternaryStart();
case 'transform':
return this.transform.bind(this);
default:
return undefined;
}
}
_getSubHandlerMethod(handlerName) {
switch (handlerName) {
case 'argVal':
return this.argVal.bind(this);
case 'arrayVal':
return this.arrayVal.bind(this);
case 'filter':
return this.filter.bind(this);
case 'objVal':
return this.objVal.bind(this);
case 'subExpression':
return this.subExpression.bind(this);
case 'ternaryEnd':
return this.ternaryEnd.bind(this);
case 'ternaryMid':
return this.ternaryMid.bind(this);
default:
return undefined;
}
}
}
//# sourceMappingURL=Parser.js.map