UNPKG

@digifi/jexl

Version:

Javascript Expression Language: Powerful context-based expression parser and evaluator

270 lines (258 loc) 10.2 kB
"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. */ return (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); } }]); }(); function isNullOrUndefinedIdentifier(value) { return value === 'null' || value === 'undefined'; } 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: nodeInProgress[nodeTransformer.transform('value')] || nodeInProgress[nodeTransformer.transform('name')], type: nodeInProgress[nodeTransformer.transform('type')], position: { start: nodeInProgress[nodeTransformer.transform('start')], end: nodeInProgress[nodeTransformer.transform('end')] } }); } else { tokens.push(nodeInProgress[nodeTransformer.transform('value')]); } } completedNodes.push(nodeInProgress); nodeInProgress = nodesQueue.shift(); } return tokens; }; Parser.findIdentifiersByAst = function (ast, includePosition) { return Parser.findTokensByAstAndTypes(ast, [TokenType.Identifier], includePosition); }; module.exports = Parser;