@digifi/jexl
Version:
Javascript Expression Language: Powerful context-based expression parser and evaluator
327 lines (272 loc) • 10.7 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
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;
var MinifyAstNodeTransformer = require('../transformers/MinifyAstNodeTransformer');
var DefaultAstNodeTransformer = require('../transformers/DefaultAstNodeTransformer');
var TokenType = require('../constants/TokenType');
/**
* 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, minify, prefix, stopMap, parentParser) {
(0, _classCallCheck2.default)(this, Parser);
this._grammar = grammar;
this._state = 'expectOperand';
this._tree = null;
this._minify = minify;
this._exprStr = prefix || '';
this._relative = false;
this._stopMap = stopMap || {};
this._parentParser = parentParser;
this.nodeTransformer = minify ? new MinifyAstNodeTransformer() : new DefaultAstNodeTransformer();
}
/**
* 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';
if (this._tree && !this._parentParser) {
this._tree.minified = this._minify;
}
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[this.nodeTransformer.transform('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, this._minify, exprStr, endStates, this);
}
}]);
return Parser;
}();
function isNullOrUndefinedIdentifier(value) {
return value === 'null' || value === 'undefined';
}
function getTokenName(node, transformer) {
var type = node[transformer.transform('type')];
if (type === TokenType.Identifier) {
return node[transformer.transform('value')];
}
if (type === TokenType.FunctionCall) {
return node[transformer.transform('value')] || node[transformer.transform('name')];
}
return null;
}
Parser.findTokensByAstAndTypes = function (ast, tokenTypes, includePosition) {
var nodeTransformer = ast.minified ? new MinifyAstNodeTransformer() : new DefaultAstNodeTransformer();
var nodesQueue = [];
var completedNodes = [];
var tokens = [];
var nodeInProgress = ast;
if (ast.minified && includePosition) {
throw new Error('Include position option is not available for minified ast');
}
while (nodeInProgress) {
if (completedNodes.includes(nodeInProgress)) {
nodeInProgress = nodesQueue.shift();
continue;
}
Object.values(nodeInProgress).forEach(function (childNode) {
if ((0, _typeof2.default)(childNode) === 'object' && childNode !== null) {
nodesQueue.push(childNode);
}
});
if (tokenTypes.includes(nodeInProgress[nodeTransformer.transform('type')]) && !nodeInProgress[nodeTransformer.transform('from')] && !isNullOrUndefinedIdentifier(nodeInProgress[nodeTransformer.transform('value')])) {
if (includePosition) {
tokens.push({
name: getTokenName(nodeInProgress, nodeTransformer),
type: nodeInProgress[nodeTransformer.transform('type')],
position: {
start: nodeInProgress[nodeTransformer.transform('start')],
end: nodeInProgress[nodeTransformer.transform('end')]
}
});
} else {
tokens.push(getTokenName(nodeInProgress, nodeTransformer));
}
}
completedNodes.push(nodeInProgress);
nodeInProgress = nodesQueue.shift();
}
return tokens;
};
Parser.findIdentifiersByAst = function (ast, includePosition) {
return Parser.findTokensByAstAndTypes(ast, [TokenType.Identifier], includePosition);
};
Parser.findFunctionNamesByAst = function (ast, includePosition) {
return Parser.findTokensByAstAndTypes(ast, [TokenType.FunctionCall], includePosition);
};
module.exports = Parser;