UNPKG

jexl-sync

Version:

(Synchronous API) Javascript Expression Language: Powerful context-based expression parser and evaluator

1,508 lines (1,421 loc) 48.4 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ var numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/, identRegex = /^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/, escEscRegex = /\\\\/, preOpRegexElems = [ // Strings "'(?:(?:\\\\')?[^'])*'", '"(?:(?:\\\\")?[^"])*"', // Whitespace '\\s+', // Booleans '\\btrue\\b', '\\bfalse\\b' ], postOpRegexElems = [ // Identifiers '\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b', // Numerics (without negative symbol) '(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)' ], minusNegatesAfter = ['binaryOp', 'unaryOp', 'openParen', 'openBracket', 'question', 'colon']; /** * Lexer is a collection of stateless, statically-accessed functions for the * lexical parsing of a Jexl string. Its responsibility is to identify the * "parts of speech" of a Jexl expression, and tokenize and label each, but * to do only the most minimal syntax checking; the only errors the Lexer * should be concerned with are if it's unable to identify the utility of * any of its tokens. Errors stemming from these tokens not being in a * sensible configuration should be left for the Parser to handle. * @type {{}} */ function Lexer(grammar) { this._grammar = grammar; } /** * Splits a Jexl expression string into an array of expression elements. * @param {string} str A Jexl expression string * @returns {Array<string>} An array of substrings defining the functional * elements of the expression. */ Lexer.prototype.getElements = function(str) { var regex = this._getSplitRegex(); return str.split(regex).filter(function(elem) { // Remove empty strings return elem; }); }; /** * Converts an array of expression elements into an array of tokens. Note that * the resulting array may not equal the element array in length, as any * elements that consist only of whitespace get appended to the previous * token's "raw" property. For the structure of a token object, please see * {@link Lexer#tokenize}. * @param {Array<string>} elements An array of Jexl expression elements to be * converted to tokens * @returns {Array<{type, value, raw}>} an array of token objects. */ Lexer.prototype.getTokens = function(elements) { var tokens = [], negate = false; for (var i = 0; i < elements.length; i++) { if (this._isWhitespace(elements[i])) { if (tokens.length) tokens[tokens.length - 1].raw += elements[i]; } else if (elements[i] === '-' && this._isNegative(tokens)) negate = true; else { if (negate) { elements[i] = '-' + elements[i]; negate = false; } tokens.push(this._createToken(elements[i])); } } // Catch a - at the end of the string. Let the parser handle that issue. if (negate) tokens.push(this._createToken('-')); return tokens; }; /** * Converts a Jexl string into an array of tokens. Each token is an object * in the following format: * * { * type: <string>, * [name]: <string>, * value: <boolean|number|string>, * raw: <string> * } * * Type is one of the following: * * literal, identifier, binaryOp, unaryOp * * OR, if the token is a control character its type is the name of the element * defined in the Grammar. * * Name appears only if the token is a control string found in * {@link grammar#elements}, and is set to the name of the element. * * Value is the value of the token in the correct type (boolean or numeric as * appropriate). Raw is the string representation of this value taken directly * from the expression string, including any trailing spaces. * @param {string} str The Jexl string to be tokenized * @returns {Array<{type, value, raw}>} an array of token objects. * @throws {Error} if the provided string contains an invalid token. */ Lexer.prototype.tokenize = function(str) { var elements = this.getElements(str); return this.getTokens(elements); }; /** * Creates a new token object from an element of a Jexl string. See * {@link Lexer#tokenize} for a description of the token object. * @param {string} element The element from which a token should be made * @returns {{value: number|boolean|string, [name]: string, type: string, * raw: string}} a token object describing the provided element. * @throws {Error} if the provided string is not a valid expression element. * @private */ Lexer.prototype._createToken = function(element) { var token = { type: 'literal', value: element, raw: element }; if (element[0] == '"' || element[0] == "'") token.value = this._unquote(element); else if (element.match(numericRegex)) token.value = parseFloat(element); else if (element === 'true' || element === 'false') token.value = element === 'true'; else if (this._grammar[element]) token.type = this._grammar[element].type; else if (element.match(identRegex)) token.type = 'identifier'; else throw new Error("Invalid expression token: " + element); return token; }; /** * Escapes a string so that it can be treated as a string literal within a * regular expression. * @param {string} str The string to be escaped * @returns {string} the RegExp-escaped string. * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions * @private */ Lexer.prototype._escapeRegExp = function(str) { str = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); if (str.match(identRegex)) str = '\\b' + str + '\\b'; return str; }; /** * Gets a RegEx object appropriate for splitting a Jexl string into its core * elements. * @returns {RegExp} An element-splitting RegExp object * @private */ Lexer.prototype._getSplitRegex = function() { if (!this._splitRegex) { var elemArray = Object.keys(this._grammar); // Sort by most characters to least, then regex escape each elemArray = elemArray.sort(function(a ,b) { return b.length - a.length; }).map(function(elem) { return this._escapeRegExp(elem); }, this); this._splitRegex = new RegExp('(' + [ preOpRegexElems.join('|'), elemArray.join('|'), postOpRegexElems.join('|') ].join('|') + ')'); } return this._splitRegex; }; /** * Determines whether the addition of a '-' token should be interpreted as a * negative symbol for an upcoming number, given an array of tokens already * processed. * @param {Array<Object>} tokens An array of tokens already processed * @returns {boolean} true if adding a '-' should be considered a negative * symbol; false otherwise * @private */ Lexer.prototype._isNegative = function(tokens) { if (!tokens.length) return true; return minusNegatesAfter.some(function(type) { return type === tokens[tokens.length - 1].type; }); }; /** * A utility function to determine if a string consists of only space * characters. * @param {string} str A string to be tested * @returns {boolean} true if the string is empty or consists of only whitespace; * false otherwise. * @private */ var _whitespaceRegex = /^\s*$/; Lexer.prototype._isWhitespace = function(str) { return _whitespaceRegex.test(str); }; /** * Removes the beginning and trailing quotes from a string, unescapes any * escaped quotes on its interior, and unescapes any escaped escape characters. * Note that this function is not defensive; it assumes that the provided * string is not empty, and that its first and last characters are actually * quotes. * @param {string} str A string whose first and last characters are quotes * @returns {string} a string with the surrounding quotes stripped and escapes * properly processed. * @private */ Lexer.prototype._unquote = function(str) { var quote = str[0], escQuoteRegex = new RegExp('\\\\' + quote, 'g'); return str.substr(1, str.length - 2) .replace(escQuoteRegex, quote) .replace(escEscRegex, '\\'); }; module.exports = Lexer; },{}],2:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ var handlers = require('./handlers'); /** * The Evaluator takes a Jexl expression tree as generated by the * {@link Parser} and calculates its value within a given context. The * collection of transforms, context, and a relative context to be used as the * root for relative identifiers, are all specific to an Evaluator instance. * When any of these things change, a new instance is required. However, a * single instance can be used to simultaneously evaluate many different * expressions, and does not have to be reinstantiated for each. * @param {{}} grammar A grammar map against which to evaluate the expression * tree * @param {{}} [transforms] A map of transform names to transform functions. A * transform function takes two arguments: * - {*} val: A value to be transformed * - {{}} args: A map of argument keys to their evaluated values, as * specified in the expression string * The transform function should return either the transformed value, or * a Promises/A+ Promise object that resolves with the value and rejects * or throws only when an unrecoverable error occurs. Transforms should * generally return undefined when they don't make sense to be used on the * given value type, rather than throw/reject. An error is only * appropriate when the transform would normally return a value, but * cannot due to some other failure. * @param {{}} [context] A map of variable keys to their values. This will be * accessed to resolve the value of each non-relative identifier. Any * Promise values will be passed to the expression as their resolved * value. * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed * to resolve the value of a relative identifier. * @constructor */ var Evaluator = function(grammar, transforms, context, relativeContext) { this._grammar = grammar; this._transforms = transforms || {}; this._context = context || {}; this._relContext = relativeContext || this._context; }; /** * Evaluates an expression tree within the configured context. * @param {{}} ast An expression tree object * @returns {Promise<*>} resolves with the resulting value of the expression. */ Evaluator.prototype.eval = function(ast) { var self = this; return handlers[ast.type].call(this, ast); // return Promise.resolve().then(function() { // return handlers[ast.type].call(self, ast); // }); }; /** * Simultaneously evaluates each expression within an array, and delivers the * response as an array with the resulting values at the same indexes as their * originating expressions. * @param {Array<string>} arr An array of expression strings to be evaluated * @returns {Promise<Array<{}>>} resolves with the result array */ Evaluator.prototype.evalArray = function(arr) { arr.map(function(elem) { return this.eval(elem); }, this) // return Promise.all(arr.map(function(elem) { // return this.eval(elem); // }, this)); }; /** * Simultaneously evaluates each expression within a map, and delivers the * response as a map with the same keys, but with the evaluated result for each * as their value. * @param {{}} map A map of expression names to expression trees to be * evaluated * @returns {Promise<{}>} resolves with the result map. */ Evaluator.prototype.evalMap = function(map) { var keys = Object.keys(map), result = {}; const vals = keys.map(function(key) { return this.eval(map[key]); }, this) vals.forEach(function(val, idx) { result[keys[idx]] = val; }); return result; // var asts = keys.map(function(key) { // return this.eval(map[key]); // }, this); // return Promise.all(asts).then(function(vals) { // vals.forEach(function(val, idx) { // result[keys[idx]] = val; // }); // return result; // }); }; /** * Applies a filter expression with relative identifier elements to a subject. * The intent is for the subject to be an array of subjects that will be * individually used as the relative context against the provided expression * tree. Only the elements whose expressions result in a truthy value will be * included in the resulting array. * * If the subject is not an array of values, it will be converted to a single- * element array before running the filter. * @param {*} subject The value to be filtered; usually an array. If this value is * not an array, it will be converted to an array with this value as the * only element. * @param {{}} expr The expression tree to run against each subject. If the * tree evaluates to a truthy result, then the value will be included in * the returned array; otherwise, it will be eliminated. * @returns {Promise<Array>} resolves with an array of values that passed the * expression filter. * @private */ Evaluator.prototype._filterRelative = function(subject, expr) { var values = []; if (!Array.isArray(subject)) subject = [subject]; subject.forEach(function(elem) { var evalInst = new Evaluator(this._grammar, this._transforms, this._context, elem); values.push(evalInst.eval(expr)); }, this); var results = []; values.forEach(function(value, idx) { if (value) results.push(subject[idx]); }); return results; // var promises = []; // if (!Array.isArray(subject)) // subject = [subject]; // subject.forEach(function(elem) { // var evalInst = new Evaluator(this._grammar, this._transforms, // this._context, elem); // promises.push(evalInst.eval(expr)); // }, this); // return Promise.all(promises).then(function(values) { // var results = []; // values.forEach(function(value, idx) { // if (value) // results.push(subject[idx]); // }); // return results; // }); }; /** * Applies a static filter expression to a subject value. If the filter * expression evaluates to boolean true, the subject is returned; if false, * undefined. * * For any other resulting value of the expression, this function will attempt * to respond with the property at that name or index of the subject. * @param {*} subject The value to be filtered. Usually an Array (for which * the expression would generally resolve to a numeric index) or an * Object (for which the expression would generally resolve to a string * indicating a property name) * @param {{}} expr The expression tree to run against the subject * @returns {Promise<*>} resolves with the value of the drill-down. * @private */ Evaluator.prototype._filterStatic = function(subject, expr) { const res = this.eval(expr) if (typeof res === 'boolean') return res ? subject : undefined; return subject[res]; // return this.eval(expr).then(function(res) { // if (typeof res === 'boolean') // return res ? subject : undefined; // return subject[res]; // }); }; module.exports = Evaluator; },{"./handlers":3}],3:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ /** * Evaluates an ArrayLiteral by returning its value, with each element * independently run through the evaluator. * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an * ObjectLiteral as the top node * @returns {Promise.<[]>} resolves to a map contained evaluated values. * @private */ exports.ArrayLiteral = function(ast) { return this.evalArray(ast.value); }; /** * Evaluates a BinaryExpression node by running the Grammar's evaluator for * the given operator. * @param {{type: 'BinaryExpression', operator: <string>, left: {}, * right: {}}} ast An expression tree with a BinaryExpression as the top * node * @returns {Promise<*>} resolves with the value of the BinaryExpression. * @private */ exports.BinaryExpression = function(ast) { var self = this; const arr = [ this.eval(ast.left), this.eval(ast.right) ] return self._grammar[ast.operator].eval(arr[0], arr[1]); // return Promise.all([ // this.eval(ast.left), // this.eval(ast.right) // ]).then(function(arr) { // return self._grammar[ast.operator].eval(arr[0], arr[1]); // }); }; /** * Evaluates a ConditionalExpression node by first evaluating its test branch, * and resolving with the consequent branch if the test is truthy, or the * alternate branch if it is not. If there is no consequent branch, the test * result will be used instead. * @param {{type: 'ConditionalExpression', test: {}, consequent: {}, * alternate: {}}} ast An expression tree with a ConditionalExpression as * the top node * @private */ exports.ConditionalExpression = function(ast) { var self = this; const res = this.eval(ast.test) if (res) { if (ast.consequent) return self.eval(ast.consequent); return res; } return self.eval(ast.alternate); // return this.eval(ast.test).then(function(res) { // if (res) { // if (ast.consequent) // return self.eval(ast.consequent); // return res; // } // return self.eval(ast.alternate); // }); }; /** * Evaluates a FilterExpression by applying it to the subject value. * @param {{type: 'FilterExpression', relative: <boolean>, expr: {}, * subject: {}}} ast An expression tree with a FilterExpression as the top * node * @returns {Promise<*>} resolves with the value of the FilterExpression. * @private */ exports.FilterExpression = function(ast) { var self = this; const subject = this.eval(ast.subject) if (ast.relative) return self._filterRelative(subject, ast.expr); return self._filterStatic(subject, ast.expr); // return this.eval(ast.subject).then(function(subject) { // if (ast.relative) // return self._filterRelative(subject, ast.expr); // return self._filterStatic(subject, ast.expr); // }); }; /** * Evaluates an Identifier by either stemming from the evaluated 'from' * expression tree or accessing the context provided when this Evaluator was * constructed. * @param {{type: 'Identifier', value: <string>, [from]: {}}} ast An expression * tree with an Identifier as the top node * @returns {Promise<*>|*} either the identifier's value, or a Promise that * will resolve with the identifier's value. * @private */ exports.Identifier = function(ast) { if (ast.from) { const context = this.eval(ast.from) if (context === undefined) return undefined; if (Array.isArray(context)) context = context[0]; return context[ast.value]; } else { return ast.relative ? this._relContext[ast.value] : this._context[ast.value]; } // if (ast.from) { // return this.eval(ast.from).then(function(context) { // if (context === undefined) // return undefined; // if (Array.isArray(context)) // context = context[0]; // return context[ast.value]; // }); // } // else { // return ast.relative ? this._relContext[ast.value] : // this._context[ast.value]; // } }; /** * Evaluates a Literal by returning its value property. * @param {{type: 'Literal', value: <string|number|boolean>}} ast An expression * tree with a Literal as its only node * @returns {string|number|boolean} The value of the Literal node * @private */ exports.Literal = function(ast) { return ast.value; }; /** * Evaluates an ObjectLiteral by returning its value, with each key * independently run through the evaluator. * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an * ObjectLiteral as the top node * @returns {Promise<{}>} resolves to a map contained evaluated values. * @private */ exports.ObjectLiteral = function(ast) { return this.evalMap(ast.value); }; /** * Evaluates a Transform node by applying a function from the transforms map * to the subject value. * @param {{type: 'Transform', name: <string>, subject: {}}} ast An * expression tree with a Transform as the top node * @returns {Promise<*>|*} the value of the transformation, or a Promise that * will resolve with the transformed value. * @private */ exports.Transform = function(ast) { var transform = this._transforms[ast.name]; if (!transform) throw new Error("Transform '" + ast.name + "' is not defined."); const arr = [ this.eval(ast.subject), this.evalArray(ast.args || []) ] return transform.apply(null, [arr[0]].concat(arr[1])); // return Promise.all([ // this.eval(ast.subject), // this.evalArray(ast.args || []) // ]).then(function(arr) { // return transform.apply(null, [arr[0]].concat(arr[1])); // }); }; /** * Evaluates a Unary expression by passing the right side through the * operator's eval function. * @param {{type: 'UnaryExpression', operator: <string>, right: {}}} ast An * expression tree with a UnaryExpression as the top node * @returns {Promise<*>} resolves with the value of the UnaryExpression. * @constructor */ exports.UnaryExpression = function(ast) { var self = this; const right = this.eval(ast.right) return self._grammar[ast.operator].eval(right); // return this.eval(ast.right).then(function(right) { // return self._grammar[ast.operator].eval(right); // }); }; },{}],4:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ var Evaluator = require('./evaluator/Evaluator'), Lexer = require('./Lexer'), Parser = require('./parser/Parser'), defaultGrammar = require('./grammar').elements; /** * Jexl is the Javascript Expression Language, capable of parsing and * evaluating basic to complex expression strings, combined with advanced * xpath-like drilldown into native Javascript objects. * @constructor */ function Jexl() { this._customGrammar = null; this._lexer = null; this._transforms = {}; } /** * Adds a binary operator to Jexl at the specified precedence. The higher the * precedence, the earlier the operator is applied in the order of operations. * For example, * has a higher precedence than +, because multiplication comes * before division. * * Please see grammar.js for a listing of all default operators and their * precedence values in order to choose the appropriate precedence for the * new operator. * @param {string} operator The operator string to be added * @param {number} precedence The operator's precedence * @param {function} fn A function to run to calculate the result. The function * will be called with two arguments: left and right, denoting the values * on either side of the operator. It should return either the resulting * value, or a Promise that resolves with the resulting value. */ Jexl.prototype.addBinaryOp = function(operator, precedence, fn) { this._addGrammarElement(operator, { type: 'binaryOp', precedence: precedence, eval: fn }); }; /** * Adds a unary operator to Jexl. Unary operators are currently only supported * on the left side of the value on which it will operate. * @param {string} operator The operator string to be added * @param {function} fn A function to run to calculate the result. The function * will be called with one argument: the literal value to the right of the * operator. It should return either the resulting value, or a Promise * that resolves with the resulting value. */ Jexl.prototype.addUnaryOp = function(operator, fn) { this._addGrammarElement(operator, { type: 'unaryOp', weight: Infinity, eval: fn }); }; /** * Adds or replaces a transform function in this Jexl instance. * @param {string} name The name of the transform function, as it will be used * within Jexl expressions * @param {function} fn The function to be executed when this transform is * invoked. It will be provided with two arguments: * - {*} value: The value to be transformed * - {{}} args: The arguments for this transform * - {function} cb: A callback function to be called with an error * if the transform fails, or a null first argument and the * transformed value as the second argument on success. */ Jexl.prototype.addTransform = function(name, fn) { this._transforms[name] = fn; }; /** * Syntactic sugar for calling {@link #addTransform} repeatedly. This function * accepts a map of one or more transform names to their transform function. * @param {{}} map A map of transform names to transform functions */ Jexl.prototype.addTransforms = function(map) { for (var key in map) { if (map.hasOwnProperty(key)) this._transforms[key] = map[key]; } }; /** * Retrieves a previously set transform function. * @param {string} name The name of the transform function * @returns {function} The transform function */ Jexl.prototype.getTransform = function(name) { return this._transforms[name]; }; /** * Evaluates a Jexl string within an optional context. * @param {string} expression The Jexl expression to be evaluated * @param {Object} [context] A mapping of variables to values, which will be * made accessible to the Jexl expression when evaluating it * @param {function} [cb] An optional callback function to be executed when * evaluation is complete. It will be supplied with two arguments: * - {Error|null} err: Present if an error occurred * - {*} result: The result of the evaluation * @returns {Promise<*>} resolves with the result of the evaluation. Note that * if a callback is supplied, the returned promise will already have * a '.catch' attached to it in order to pass the error to the callback. */ Jexl.prototype.eval = function(expression, context, cb) { if (typeof context === 'function') { cb = context; context = {}; } else if (!context) context = {}; try { const val = this._eval(expression, context); } catch(err) { if (!called) setTimeout(cb.bind(null, err), 0); } if (cb) { // setTimeout is used for the callback to break out of the Promise's // try/catch in case the callback throws. var called = false; called = true; setTimeout(cb.bind(null, null, val), 0); return val } return val; // var valPromise = this._eval(expression, context); // if (cb) { // // setTimeout is used for the callback to break out of the Promise's // // try/catch in case the callback throws. // var called = false; // return valPromise.then(function(val) { // called = true; // setTimeout(cb.bind(null, null, val), 0); // }).catch(function(err) { // if (!called) // setTimeout(cb.bind(null, err), 0); // }); // } // return valPromise; }; /** * Removes a binary or unary operator from the Jexl grammar. * @param {string} operator The operator string to be removed */ Jexl.prototype.removeOp = function(operator) { var grammar = this._getCustomGrammar(); if (grammar[operator] && (grammar[operator].type == 'binaryOp' || grammar[operator].type == 'unaryOp')) { delete grammar[operator]; this._lexer = null; } }; /** * Adds an element to the grammar map used by this Jexl instance, cloning * the default grammar first if necessary. * @param {string} str The key string to be added * @param {{type: <string>}} obj A map of configuration options for this * grammar element * @private */ Jexl.prototype._addGrammarElement = function(str, obj) { var grammar = this._getCustomGrammar(); grammar[str] = obj; this._lexer = null; }; /** * Evaluates a Jexl string in the given context. * @param {string} exp The Jexl expression to be evaluated * @param {Object} [context] A mapping of variables to values, which will be * made accessible to the Jexl expression when evaluating it * @returns {Promise<*>} resolves with the result of the evaluation. * @private */ Jexl.prototype._eval = function(exp, context) { var self = this, grammar = this._getGrammar(), parser = new Parser(grammar), evaluator = new Evaluator(grammar, this._transforms, context); parser.addTokens(self._getLexer().tokenize(exp)); return evaluator.eval(parser.complete()); // return Promise.resolve().then(function() { // parser.addTokens(self._getLexer().tokenize(exp)); // return evaluator.eval(parser.complete()); // }); }; /** * Gets the custom grammar object, creating it first if necessary. New custom * grammars are created by executing a shallow clone of the default grammar * map. The returned map is available to be changed. * @returns {{}} a customizable grammar map. * @private */ Jexl.prototype._getCustomGrammar = function() { if (!this._customGrammar) { this._customGrammar = {}; for (var key in defaultGrammar) { if (defaultGrammar.hasOwnProperty(key)) this._customGrammar[key] = defaultGrammar[key]; } } return this._customGrammar; }; /** * Gets the grammar map currently being used by Jexl; either the default map, * or a locally customized version. The returned map should never be changed * in any way. * @returns {{}} the grammar map currently in use. * @private */ Jexl.prototype._getGrammar = function() { return this._customGrammar || defaultGrammar; }; /** * Gets a Lexer instance as a singleton in reference to this Jexl instance. * @returns {Lexer} an instance of Lexer, initialized with a grammar * appropriate to this Jexl instance. * @private */ Jexl.prototype._getLexer = function() { if (!this._lexer) this._lexer = new Lexer(this._getGrammar()); return this._lexer; }; Jexl.prototype.parse = function (exp) { const grammar = this._getGrammar() const parser = new Parser(grammar) parser.addTokens(this._getLexer().tokenize(exp)); return parser.complete() } Jexl.prototype.evaluate = function (parserTree, data) { const grammar = this._getGrammar() const evaluator = new Evaluator(grammar, {}, data) return evaluator.eval(parserTree) } module.exports = new Jexl(); module.exports.Jexl = Jexl; },{"./Lexer":1,"./evaluator/Evaluator":2,"./grammar":5,"./parser/Parser":6}],5:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ /** * A map of all expression elements to their properties. Note that changes * here may require changes in the Lexer or Parser. * @type {{}} */ exports.elements = { '.': {type: 'dot'}, '[': {type: 'openBracket'}, ']': {type: 'closeBracket'}, '|': {type: 'pipe'}, '{': {type: 'openCurl'}, '}': {type: 'closeCurl'}, ':': {type: 'colon'}, ',': {type: 'comma'}, '(': {type: 'openParen'}, ')': {type: 'closeParen'}, '?': {type: 'question'}, '+': {type: 'binaryOp', precedence: 30, eval: function(left, right) { return left + right; }}, '-': {type: 'binaryOp', precedence: 30, eval: function(left, right) { return left - right; }}, '*': {type: 'binaryOp', precedence: 40, eval: function(left, right) { return left * right; }}, '/': {type: 'binaryOp', precedence: 40, eval: function(left, right) { return left / right; }}, '//': {type: 'binaryOp', precedence: 40, eval: function(left, right) { return Math.floor(left / right); }}, '%': {type: 'binaryOp', precedence: 50, eval: function(left, right) { return left % right; }}, '^': {type: 'binaryOp', precedence: 50, eval: function(left, right) { return Math.pow(left, right); }}, '==': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left == right; }}, '!=': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left != right; }}, '>': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left > right; }}, '>=': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left >= right; }}, '<': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left < right; }}, '<=': {type: 'binaryOp', precedence: 20, eval: function(left, right) { return left <= right; }}, '&&': {type: 'binaryOp', precedence: 10, eval: function(left, right) { return left && right; }}, '||': {type: 'binaryOp', precedence: 10, eval: function(left, right) { return left || right; }}, 'in': {type: 'binaryOp', precedence: 20, eval: function(left, right) { if (typeof right === 'string') return right.indexOf(left) !== -1; if (Array.isArray(right)) { return right.some(function(elem) { return elem == left; }); } return false; }}, '!': {type: 'unaryOp', precedence: Infinity, eval: function(right) { return !right; }} }; },{}],6:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ var handlers = require('./handlers'), 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 map 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. * @constructor */ function Parser(grammar, prefix, stopMap) { 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. */ Parser.prototype.addToken = function(token) { if (this._state == 'complete') throw new Error('Cannot add a new token to a completed Parser'); var state = states[this._state], 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], 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 ' + token.raw + ' (' + token.type + ') unexpected in expression: ' + 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. */ Parser.prototype.addTokens = function(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 */ Parser.prototype.complete = function() { if (this._cursor && !states[this._state].completable) throw new Error('Unexpected end of expression: ' + 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. */ Parser.prototype.isRelative = function() { return this._relative; }; /** * Ends a subexpression by completing the subParser and passing its result * to the subHandler configured in the current state. * @private */ Parser.prototype._endSubExpression = function() { 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 */ Parser.prototype._placeAtCursor = function(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 */ Parser.prototype._placeBeforeCursor = function(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 */ Parser.prototype._setParent = function(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 */ Parser.prototype._startSubExpression = function(exprStr) { var endStates = states[this._state].endStates; if (!endStates) { this._parentStop = true; endStates = this._stopMap; } this._subParser = new Parser(this._grammar, exprStr, endStates); }; module.exports = Parser; },{"./handlers":7,"./states":8}],7:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ /** * Handles a subexpression that's used to define a transform argument's value. * @param {{type: <string>}} ast The subexpression tree */ exports.argVal = function(ast) { this._cursor.args.push(ast); }; /** * Handles new array literals by adding them as a new node in the AST, * initialized with an empty array. */ exports.arrayStart = function() { this._placeAtCursor({ type: 'ArrayLiteral', value: [] }); }; /** * Handles a subexpression representing an element of an array literal. * @param {{type: <string>}} ast The subexpression tree */ exports.arrayVal = function(ast) { if (ast) this._cursor.value.push(ast); }; /** * Handles tokens of type 'binaryOp', indicating an operation that has two * inputs: a left side and a right side. * @param {{type: <string>}} token A token object */ exports.binaryOp = function(token) { var precedence = this._grammar[token.value].precedence || 0, parent = this._cursor._parent; while (parent && parent.operator && this._grammar[parent.operator].precedence >= precedence) { this._cursor = parent; parent = parent._parent; } var node = { type: 'BinaryExpression', operator: token.value, left: this._cursor }; this._setParent(this._cursor, node); this._cursor = parent; this._placeAtCursor(node); }; /** * Handles successive nodes in an identifier chain. More specifically, it * sets values that determine how the following identifier gets placed in the * AST. */ exports.dot = function() { this._nextIdentEncapsulate = this._cursor && (this._cursor.type != 'BinaryExpression' || (this._cursor.type == 'BinaryExpression' && this._cursor.right)) && this._cursor.type != 'UnaryExpression'; this._nextIdentRelative = !this._cursor || (this._cursor && !this._nextIdentEncapsulate); if (this._nextIdentRelative) this._relative = true; }; /** * Handles a subexpression used for filtering an array returned by an * identifier chain. * @param {{type: <string>}} ast The subexpression tree */ exports.filter = function(ast) { this._placeBeforeCursor({ type: 'FilterExpression', expr: ast, relative: this._subParser.isRelative(), subject: this._cursor }); }; /** * Handles identifier tokens by adding them as a new node in the AST. * @param {{type: <string>}} token A token object */ exports.identifier = function(token) { var node = { type: 'Identifier', value: token.value }; if (this._nextIdentEncapsulate) { node.from = this._cursor; this._placeBeforeCursor(node); this._nextIdentEncapsulate = false; } else { if (this._nextIdentRelative) node.relative = true; this._placeAtCursor(node); } }; /** * Handles literal values, such as strings, booleans, and numerics, by adding * them as a new node in the AST. * @param {{type: <string>}} token A token object */ exports.literal = function(token) { this._placeAtCursor({ type: 'Literal', value: token.value }); }; /** * Queues a new object literal key to be written once a value is collected. * @param {{type: <string>}} token A token object */ exports.objKey = function(token) { this._curObjKey = token.value; }; /** * Handles new object literals by adding them as a new node in the AST, * initialized with an empty object. */ exports.objStart = function() { this._placeAtCursor({ type: 'ObjectLiteral', value: {} }); }; /** * Handles an object value by adding its AST to the queued key on the object * literal node currently at the cursor. * @param {{type: <string>}} ast The subexpression tree */ exports.objVal = function(ast) { this._cursor.value[this._curObjKey] = ast; }; /** * Handles traditional subexpressions, delineated with the groupStart and * groupEnd elements. * @param {{type: <string>}} ast The subexpression tree */ exports.subExpression = function(ast) { this._placeAtCursor(ast); }; /** * Handles a completed alternate subexpression of a ternary operator. * @param {{type: <string>}} ast The subexpression tree */ exports.ternaryEnd = function(ast) { this._cursor.alternate = ast; }; /** * Handles a completed consequent subexpression of a ternary operator. * @param {{type: <string>}} ast The subexpression tree */ exports.ternaryMid = function(ast) { this._cursor.consequent = ast; }; /** * Handles the start of a new ternary expression by encapsulating the entire * AST in a ConditionalExpression node, and using the existing tree as the * test element. */ exports.ternaryStart = function() { this._tree = { type: 'ConditionalExpression', test: this._tree }; this._cursor = this._tree; }; /** * Handles identifier tokens when used to indicate the name of a transform to * be applied. * @param {{type: <string>}} token A token object */ exports.transform = function(token) { this._placeBeforeCursor({ type: 'Transform', name: token.value, args: [], subject: this._cursor }); }; /** * Handles token of type 'unaryOp', indicating that the operation has only * one input: a right side. * @param {{type: <string>}} token A token object */ exports.unaryOp = function(token) { this._placeAtCursor({ type: 'UnaryExpression', operator: token.value }); }; },{}],8:[function(require,module,exports){ /* * Jexl * Copyright (c) 2017 Tom Shawver */ var h = require('./handlers'); /** * A mapping of all states in the finite state machine to a set of instructions * for handling or transitioning into other states. Each state can be handled * in one of two schemes: a tokenType map, or a subHandler. * * Standard expression elements are handled through the tokenType object. This * is an object map of all legal token types to encounter in this state (and * any unexpected token types will generate a thrown error) to an options * object that defines how they're handled. The available options are: * * {string} toState: The name of the state to which to transition * immediately after handling this token * {string} handler: The handler function to call when this token type is * encountered in this state. If omitted, the default handler * matching the token's "type" property will be called. If the handler * function does not exist, no call will be made and no error will be * generated. This is useful for tokens whose sole purpose is to * transition to other states. * * States that consume a subexpression should define a subHandler, the * function to be called with an expression tree argument when the * subexpression is complete. Completeness is determined through the * endStates object, which maps tokens on which an expression should end to the * state to which to transition once the subHandler function has been called. * * Additionally, any state in which it is legal to mark the AST as completed * should have a 'completable' property set to boolean true. Attempting to * call {@link Parser#complete} in any state without this property will result * in a thrown Error. * * @type {{}} */ exports.states = { expectOperand: { tokenTypes: { literal: {toState: 'expectBinOp'}, identifier: {toState: 'identifier'}, unaryOp: {}, openParen: {toState: 'subExpression'}, openCurl: {toState: 'expectObjKey', handler: h.objStart}, dot: {toState: 'traverse'}, openBracket: {toState: 'arrayVal', handler: h.arrayStart} } }, expectBinOp: { tokenTypes: { binaryOp: {toState: 'expectOperand'}, pipe: {toState: 'expectTransform'}, dot: {toState: 'traverse'}, question: {toState: 'ternaryMid', handler: h.ternaryStart} }, completable: true }, expectTransform: { tokenTypes: { identifier: {toState: 'postTransform', handler: h.transform} } }, expectObjKey: { tokenTypes: { identifier: {toState: 'expectKeyValSep', handler: h.objKey}, closeCurl: {toState: 'expectBinOp'} } }, expectKeyValSep: { tokenTypes: { colon: {toState: 'objVal'} } }, postTransform: { tokenTypes: { openParen: {toState: 'argVal'}, binaryOp: {toState: 'expectOperand'}, dot: {toState: 'traverse'}, openBracket: {toState: 'filter'}, pipe: {toState: 'expectTransform'} }, completable: true }, postTransformArgs: { tokenTypes: { binaryOp: {toState: 'expectOperand'}, dot: {toState: 'traverse'}, openBracket: {toState: 'filter'}, pipe: {toState: 'expectTransform'} }, completable: true }, identifier: { tokenTypes: { binaryOp: {toState: 'expectOperand'}, dot: {toState: 'traverse'}, openBracket: {toState: 'filter'}, pipe: {toState: 'expectTransform'}, question: {toState: 'ternaryMid', handler: h.ternaryStart} }, completable: true }, traverse: { tokenTypes: { 'identifier': {toState: 'identifier'} } }, filter: { subHandler: h.filter, endStates: { closeBracket: 'identifier' } }, subExpression: { subHandler: h.subExpression, endStates: { closeParen: 'expectBinOp' } }, argVal: { subHandler: h.argVal, endStates: { comma: 'argVal', closeParen: 'postTransformArgs' } }, objVal: { subHandler: h.objVal, endStates: { comma: 'expectObjKey', closeCurl: 'expectBinOp' } }, arrayVal: { subHandler: h.arrayVal, endStates: { comma: 'arrayVal', closeBracket: 'expectBinOp' } }, ternaryMid: { subHandler: h.ternaryMid, endStates: { colon: 'ternaryEnd' } }, ternaryEnd: { subHandler: h.ternaryEnd, completable: true } }; },{"./handlers":7}]},{},[4])