@firehammer/jexl
Version:
Javascript Expression Language: Powerful context-based expression parser and evaluator
217 lines (205 loc) • 7.75 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
/*
* Jexl
* Copyright 2020 Tom Shawver
*/
var handlers = require("./handlers");
var states = require("./states").states;
/**
* The Parser is a state machine that converts tokens from the {@link Lexer}
* into an Abstract Syntax Tree (AST), capable of being evaluated in any
* context by the {@link Evaluator}. The Parser expects that all tokens
* provided to it are legal and typed properly according to the grammar, but
* accepts that the tokens may still be in an invalid order or in some other
* unparsable configuration that requires it to throw an Error.
* @param {{}} grammar The grammar object to use to parse Jexl strings
* @param {string} [prefix] A string prefix to prepend to the expression string
* for error messaging purposes. This is useful for when a new Parser is
* instantiated to parse an subexpression, as the parent Parser's
* expression string thus far can be passed for a more user-friendly
* error message.
* @param {{}} [stopMap] A mapping of token types to any truthy value. When the
* token type is encountered, the parser will return the mapped value
* instead of boolean false.
*/
var Parser = /*#__PURE__*/function () {
function Parser(grammar, prefix, stopMap) {
(0, _classCallCheck2.default)(this, Parser);
this._grammar = grammar;
this._state = "expectOperand";
this._tree = null;
this._exprStr = prefix || "";
this._relative = false;
this._stopMap = stopMap || {};
}
/**
* Processes a new token into the AST and manages the transitions of the state
* machine.
* @param {{type: <string>}} token A token object, as provided by the
* {@link Lexer#tokenize} function.
* @throws {Error} if a token is added when the Parser has been marked as
* complete by {@link #complete}, or if an unexpected token type is added.
* @returns {boolean|*} the stopState value if this parser encountered a token
* in the stopState mapb false if tokens can continue.
*/
(0, _createClass2.default)(Parser, [{
key: "addToken",
value: function addToken(token) {
if (this._state === "complete") {
throw new Error("Cannot add a new token to a completed Parser");
}
var state = states[this._state];
var startExpr = this._exprStr;
this._exprStr += token.raw;
if (state.subHandler) {
if (!this._subParser) {
this._startSubExpression(startExpr);
}
var stopState = this._subParser.addToken(token);
if (stopState) {
this._endSubExpression();
if (this._parentStop) return stopState;
this._state = stopState;
}
} else if (state.tokenTypes[token.type]) {
var typeOpts = state.tokenTypes[token.type];
var handleFunc = handlers[token.type];
if (typeOpts.handler) {
handleFunc = typeOpts.handler;
}
if (handleFunc) {
handleFunc.call(this, token);
}
if (typeOpts.toState) {
this._state = typeOpts.toState;
}
} else if (this._stopMap[token.type]) {
return this._stopMap[token.type];
} else {
throw new Error("Token ".concat(token.raw, " (").concat(token.type, ") unexpected in expression: ").concat(this._exprStr));
}
return false;
}
/**
* Processes an array of tokens iteratively through the {@link #addToken}
* function.
* @param {Array<{type: <string>}>} tokens An array of tokens, as provided by
* the {@link Lexer#tokenize} function.
*/
}, {
key: "addTokens",
value: function addTokens(tokens) {
tokens.forEach(this.addToken, this);
}
/**
* Marks this Parser instance as completed and retrieves the full AST.
* @returns {{}|null} a full expression tree, ready for evaluation by the
* {@link Evaluator#eval} function, or null if no tokens were passed to
* the parser before complete was called
* @throws {Error} if the parser is not in a state where it's legal to end
* the expression, indicating that the expression is incomplete
*/
}, {
key: "complete",
value: function complete() {
if (this._cursor && !states[this._state].completable) {
throw new Error("Unexpected end of expression: ".concat(this._exprStr));
}
if (this._subParser) {
this._endSubExpression();
}
this._state = "complete";
return this._cursor ? this._tree : null;
}
/**
* Indicates whether the expression tree contains a relative path identifier.
* @returns {boolean} true if a relative identifier exists false otherwise.
*/
}, {
key: "isRelative",
value: function isRelative() {
return this._relative;
}
/**
* Ends a subexpression by completing the subParser and passing its result
* to the subHandler configured in the current state.
* @private
*/
}, {
key: "_endSubExpression",
value: function _endSubExpression() {
states[this._state].subHandler.call(this, this._subParser.complete());
this._subParser = null;
}
/**
* Places a new tree node at the current position of the cursor (to the 'right'
* property) and then advances the cursor to the new node. This function also
* handles setting the parent of the new node.
* @param {{type: <string>}} node A node to be added to the AST
* @private
*/
}, {
key: "_placeAtCursor",
value: function _placeAtCursor(node) {
if (!this._cursor) {
this._tree = node;
} else {
this._cursor.right = node;
this._setParent(node, this._cursor);
}
this._cursor = node;
}
/**
* Places a tree node before the current position of the cursor, replacing
* the node that the cursor currently points to. This should only be called in
* cases where the cursor is known to exist, and the provided node already
* contains a pointer to what's at the cursor currently.
* @param {{type: <string>}} node A node to be added to the AST
* @private
*/
}, {
key: "_placeBeforeCursor",
value: function _placeBeforeCursor(node) {
this._cursor = this._cursor._parent;
this._placeAtCursor(node);
}
/**
* Sets the parent of a node by creating a non-enumerable _parent property
* that points to the supplied parent argument.
* @param {{type: <string>}} node A node of the AST on which to set a new
* parent
* @param {{type: <string>}} parent An existing node of the AST to serve as the
* parent of the new node
* @private
*/
}, {
key: "_setParent",
value: function _setParent(node, parent) {
Object.defineProperty(node, "_parent", {
value: parent,
writable: true
});
}
/**
* Prepares the Parser to accept a subexpression by (re)instantiating the
* subParser.
* @param {string} [exprStr] The expression string to prefix to the new Parser
* @private
*/
}, {
key: "_startSubExpression",
value: function _startSubExpression(exprStr) {
var endStates = states[this._state].endStates;
if (!endStates) {
this._parentStop = true;
endStates = this._stopMap;
}
this._subParser = new Parser(this._grammar, exprStr, endStates);
}
}]);
return Parser;
}();
module.exports = Parser;