formulax
Version:
Fast javascript expression parser and compiler.
856 lines (751 loc) • 23.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _utils = require('./utils');
var isProd = process.env.NODE_ENV === 'production';
var COMPOUND = isProd ? 1 : 'Compound';
var IDENTIFIER = isProd ? 2 : 'Identifier';
var MEMBER_EXP = isProd ? 3 : 'MemberExpression';
var LITERAL = isProd ? 4 : 'Literal';
var THIS_EXP = isProd ? 5 : 'ThisExpression';
var CALL_EXP = isProd ? 6 : 'CallExpression';
var UNARY_EXP = isProd ? 7 : 'UnaryExpression';
var BINARY_EXP = isProd ? 8 : 'BinaryExpression';
var LOGICAL_EXP = isProd ? 9 : 'LogicalExpression';
var CONDITIONAL_EXP = isProd ? 10 : 'ConditionalExpression';
var ARRAY_EXP = isProd ? 11 : 'ArrayExpression';
var PERIOD_CODE = 46; // '.'
var COMMA_CODE = 44; // ','
var SQUOTE_CODE = 39; // single quote
var DQUOTE_CODE = 34; // double quotes
var OPAREN_CODE = 40; // (
var CPAREN_CODE = 41; // )
var OBRACK_CODE = 91; // [
var CBRACK_CODE = 93; // ]
var QUMARK_CODE = 63; // ?
var SEMCOL_CODE = 59; // ;
var COLON_CODE = 58; // :
// Operations
//
var t = true;
var OPERATOR_PLUS = '+';
var OPERATOR_MINUS = '-';
var OPERATOR_DIVISION = '/';
var OPERATOR_MULTIPLE = '*';
var OPERATOR_POW = '^';
var OPERATOR_GT = '>';
var OPERATOR_GTE = '>=';
var OPERATOR_LT = '<';
var OPERATOR_LTE = '<=';
var OPERATOR_NOT = '!';
var OPERATOR_BOOL = '!!';
var OPERATOR_INT = '~~';
//
var evals = {};
evals[OPERATOR_PLUS] = function (a, b) {
return a + b;
};
evals[OPERATOR_MINUS] = function (a, b) {
return a - b;
};
evals[OPERATOR_DIVISION] = function (a, b) {
return a / b;
};
evals[OPERATOR_MULTIPLE] = function (a, b) {
return a * b;
};
evals[OPERATOR_POW] = function (a, b) {
return Math.pow(a, b);
};
evals[OPERATOR_GT] = function (a, b) {
return a > b;
};
evals[OPERATOR_GTE] = function (a, b) {
return a >= b;
};
evals[OPERATOR_LT] = function (a, b) {
return a < b;
};
evals[OPERATOR_LTE] = function (a, b) {
return a <= b;
};
evals[OPERATOR_NOT] = function (x) {
return !x;
};
evals[OPERATOR_BOOL] = function (x) {
return !!x;
};
evals[OPERATOR_INT] = function (x) {
return ~~x;
};
// Use a quickly-accessible map to store all of the unary operators
var unary_ops = {
'-': 1,
'!': 1,
'~': 1,
'+': 1
};
var literals = {
'true': true,
'false': false,
'null': null
// Also use a map for the binary operations but set their values to their
// binary precedence for quick reference:
// see [Order of operations](http://en.wikipedia.org/wiki/Order_of_operations#Programming_language)
};var binary_ops = {
'->': 0,
'||': 1,
'&&': 2,
'|': 3,
'^': 4,
'&': 5,
'==': 6,
'!=': 6,
'===': 6,
'!==': 6,
'<': 7,
'>': 7,
'<=': 7,
'>=': 7,
'<<': 8,
'>>': 8,
'>>>': 8,
'+': 9,
'-': 9,
'*': 10,
'/': 10,
'%': 10
};
var max_unop_len = (0, _utils.getMaxKeyLen)(unary_ops);
var max_binop_len = (0, _utils.getMaxKeyLen)(binary_ops);
// Literals
// ----------
// Store the values to return for the various literals we may encounter
// Except for `this`, which is special. This could be changed to something like `'self'` as well
var this_str = 'this';
// Returns the precedence of a binary operator or `0` if it isn't a binary operator
var binaryPrecedence = function binaryPrecedence(op_val) {
return binary_ops[op_val] || 0;
};
// Utility function (gets called from multiple places)
// Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions
var createBinaryExpression = function createBinaryExpression(operator, left, right) {
return {
operator: operator, left: left, right: right,
type: operator === '||' || operator === '&&' ? LOGICAL_EXP : BINARY_EXP
};
};
// `ch` is a character code in the next three functions
var isDecimalDigit = function isDecimalDigit(ch) {
return ch >= 48 && ch <= 57;
};
var isIdentifierStart = function isIdentifierStart(ch) {
return ch === 36 || ch === 95 || // `$` and `_`
ch >= 65 && ch <= 90 || // A...Z
ch >= 97 && ch <= 122 || // a...z
ch >= 128 && !binary_ops[String.fromCharCode(ch)] // any non-ASCII that is not an operator
;
};
var isIdentifierPart = function isIdentifierPart(ch, partial) {
return ch === 36 || ch === 95 || // `$` and `_`
ch >= 65 && ch <= 90 || // A...Z
ch >= 97 && ch <= 122 || // a...z
ch >= 48 && ch <= 57 || // 0...9
ch >= 128 && !binary_ops[String.fromCharCode(ch)] // any non-ASCII that is not an operator
;
};
var exprIBuild = function exprIBuild(expr) {
var charAtFunc = expr.charAt;
return function (index) {
return charAtFunc.call(expr, index);
};
};
var exprICodeBuild = function exprICodeBuild(expr) {
var charCodeAtFunc = expr.charCodeAt;
return function (index) {
return charCodeAtFunc.call(expr, index);
};
};
var gobbleSpaces = function gobbleSpaces(expr, index) {
var ch = expr.charCodeAt(index);
while (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
// space or tab
ch = expr.charAt(++index);
}
return index;
};
// Search for the operation portion of the string (e.g. `+`, `===`)
// Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
// and move down from 3 to 2 to 1 character until a matching binary operation is found
// then, return that binary operation
var gobbleBinaryOp = function gobbleBinaryOp(expr, index) {
index = gobbleSpaces(expr, index);
var to_check = expr.substr(index, max_binop_len);
var tc_len = to_check.length;
while (tc_len > 0) {
if (binary_ops.hasOwnProperty(to_check)) {
index += tc_len;
return { to_check: to_check, index: index };
}
to_check = to_check.substr(0, --tc_len);
}
return { index: index };
};
// Parsing
// -------
// `expr` is a string with the passed in expression
var parser = function parser(expr) {
// const tmp = expr.split('->')
// expr = tmp.map((e, i) => {
// return '('.repeat(tmp.length - i) + e + ')'
// }).join('')
// `index` stores the character number we are currently at while `length` is a constant
// All of the gobbles below will modify `index` as we move along
var index = 0;
var exprI = exprIBuild(expr);
var exprICode = exprICodeBuild(expr);
var length = expr.length;
// Push `index` up to the next non-space character
// The main parsing function. Much of this code is dedicated to ternary expressions
var gobbleExpression = function gobbleExpression() {
var test = gobbleBinaryExpression(),
consequent,
alternate;
index = gobbleSpaces(expr, index);
if (exprICode(index) === QUMARK_CODE) {
// Ternary expression: test ? consequent : alternate
index++;
consequent = gobbleExpression();
if (!consequent) {
(0, _utils.throwError)('Expected expression', index);
}
index = gobbleSpaces(expr, index);
if (exprICode(index) === COLON_CODE) {
index++;
alternate = gobbleExpression();
if (!alternate) {
(0, _utils.throwError)('Expected expression', index);
}
return {
type: CONDITIONAL_EXP,
test: test,
consequent: consequent,
alternate: alternate
};
} else {
(0, _utils.throwError)('Expected :', index);
}
} else {
return test;
}
},
// This function is responsible for gobbling an individual expression,
// e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
gobbleBinaryExpression = function gobbleBinaryExpression() {
var node, biop, prec, stack, biop_info, left, right, i;
// First, try to get the leftmost thing
// Then, check to see if there's a binary operator operating on that leftmost thing
left = gobbleToken();
biop = gobbleBinaryOp(expr, index);
index = biop.index;
// If there wasn't a binary operator, just return the leftmost node
if (!biop.to_check) {
return left;
}
// Otherwise, we need to start a stack to properly place the binary operations in their
// precedence structure
biop_info = { value: biop.to_check, prec: binaryPrecedence(biop.to_check) };
right = gobbleToken();
if (!right) {
(0, _utils.throwError)('Expected expression after ' + biop.to_check, index);
}
stack = [left, biop_info, right];
// Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
while (biop = gobbleBinaryOp(expr, index)) {
index = biop.index;
prec = binaryPrecedence(biop.to_check);
if (prec === 0) {
break;
}
biop_info = { value: biop.to_check, prec: prec
// Reduce: make a binary expression from the three topmost entries.
};while (stack.length > 2 && prec <= stack[stack.length - 2].prec) {
right = stack.pop();
biop = stack.pop().value;
left = stack.pop();
node = createBinaryExpression(biop, left, right);
stack.push(node);
}
node = gobbleToken();
if (!node) {
(0, _utils.throwError)('Expected expression after ' + biop, index);
}
stack.push(biop_info, node);
}
i = stack.length - 1;
node = stack[i];
while (i > 1) {
node = createBinaryExpression(stack[i - 1].value, stack[i - 2], node);
i -= 2;
}
return node;
},
// An individual part of a binary expression:
// e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
gobbleToken = function gobbleToken() {
var ch, to_check, tc_len;
index = gobbleSpaces(expr, index);
ch = exprICode(index);
if (isDecimalDigit(ch) || ch === PERIOD_CODE) {
// Char code 46 is a dot `.` which can start off a numeric literal
return gobbleNumericLiteral();
} else if (ch === SQUOTE_CODE || ch === DQUOTE_CODE) {
// Single or double quotes
return gobbleStringLiteral();
} else if (ch === OBRACK_CODE) {
return gobbleArray();
} else {
to_check = expr.substr(index, max_unop_len);
tc_len = to_check.length;
while (tc_len > 0) {
if (unary_ops.hasOwnProperty(to_check)) {
index += tc_len;
return {
type: UNARY_EXP,
operator: to_check,
argument: gobbleToken(),
prefix: true
};
}
to_check = to_check.substr(0, --tc_len);
}
if (isIdentifierStart(ch) || ch === OPAREN_CODE) {
// open parenthesis
// `foo`, `bar.baz`
return gobbleVariable();
}
}
return false;
},
// Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
// keep track of everything in the numeric literal and then calling `parseFloat` on that string
gobbleNumericLiteral = function gobbleNumericLiteral() {
var number = '',
ch,
chCode;
while (isDecimalDigit(exprICode(index))) {
number += exprI(index++);
}
if (exprICode(index) === PERIOD_CODE) {
// can start with a decimal marker
number += exprI(index++);
while (isDecimalDigit(exprICode(index))) {
number += exprI(index++);
}
}
ch = exprI(index);
if (ch === 'e' || ch === 'E') {
// exponent marker
number += exprI(index++);
ch = exprI(index);
if (ch === '+' || ch === '-') {
// exponent sign
number += exprI(index++);
}
while (isDecimalDigit(exprICode(index))) {
//exponent itself
number += exprI(index++);
}
if (!isDecimalDigit(exprICode(index - 1))) {
(0, _utils.throwError)('Expected exponent (' + number + exprI(index) + ')', index);
}
}
chCode = exprICode(index);
// Check to make sure this isn't a variable name that start with a number (123abc)
if (isIdentifierStart(chCode)) {
(0, _utils.throwError)('Variable names cannot start with a number (' + number + exprI(index) + ')', index);
} else if (chCode === PERIOD_CODE) {
(0, _utils.throwError)('Unexpected period', index);
}
return {
type: LITERAL,
value: parseFloat(number),
raw: number
};
},
// Parses a string literal, staring with single or double quotes with basic support for escape codes
// e.g. `"hello world"`, `'this is\nparser'`
gobbleStringLiteral = function gobbleStringLiteral() {
var str = '',
quote = exprI(index++),
closed = false,
ch;
while (index < length) {
ch = exprI(index++);
if (ch === quote) {
closed = true;
break;
} else if (ch === '\\') {
// Check for all of the common escape codes
ch = exprI(index++);
switch (ch) {
case 'n':
str += '\n';break;
case 'r':
str += '\r';break;
case 't':
str += '\t';break;
case 'b':
str += '\b';break;
case 'f':
str += '\f';break;
case 'v':
str += '\x0B';break;
default:
str += ch;
}
} else {
str += ch;
}
}
if (!closed) {
(0, _utils.throwError)('Unclosed quote after "' + str + '"', index);
}
return {
type: LITERAL,
value: str,
raw: quote + str + quote
};
},
// Gobbles only identifiers
// e.g.: `foo`, `_value`, `$x1`
// Also, this function checks if that identifier is a literal:
// (e.g. `true`, `false`, `null`) or `this`
gobbleIdentifier = function gobbleIdentifier() {
var ch = exprICode(index),
start = index,
identifier;
if (isIdentifierStart(ch)) {
index++;
} else {
(0, _utils.throwError)('Unexpected ' + exprI(index), index);
}
while (index < length) {
ch = exprICode(index);
if (isIdentifierPart(ch)) {
index++;
} else {
break;
}
}
identifier = expr.slice(start, index);
if (literals.hasOwnProperty(identifier)) {
return {
type: LITERAL,
value: literals[identifier],
raw: identifier
};
} else if (identifier === this_str) {
return { type: THIS_EXP };
} else {
return {
type: IDENTIFIER,
name: identifier
};
}
},
// Gobbles a list of arguments within the context of a function call
// or array literal. This function also assumes that the opening character
// `(` or `[` has already been gobbled, and gobbles expressions and commas
// until the terminator character `)` or `]` is encountered.
// e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
gobbleArguments = function gobbleArguments(termination) {
var ch_i,
args = [],
node,
closed = false;
while (index < length) {
index = gobbleSpaces(expr, index);
ch_i = exprICode(index);
if (ch_i === termination) {
// done parsing
closed = true;
index++;
break;
} else if (ch_i === COMMA_CODE) {
// between expressions
index++;
} else {
node = gobbleExpression();
if (!node || node.type === COMPOUND) {
(0, _utils.throwError)('Expected comma', index);
}
args.push(node);
}
}
if (!closed) {
(0, _utils.throwError)('Expected ' + String.fromCharCode(termination), index);
}
return args;
},
// Gobble a non-literal variable name. This variable name may include properties
// e.g. `foo`, `bar.baz`, `foo['bar'].baz`
// It also gobbles function calls:
// e.g. `Math.acos(obj.angle)`
gobbleVariable = function gobbleVariable() {
var ch_i, node;
ch_i = exprICode(index);
if (ch_i === OPAREN_CODE) {
node = gobbleGroup();
} else {
node = gobbleIdentifier();
}
index = gobbleSpaces(expr, index);
ch_i = exprICode(index);
while (ch_i === PERIOD_CODE || ch_i === OBRACK_CODE || ch_i === OPAREN_CODE) {
index++;
if (ch_i === PERIOD_CODE) {
index = gobbleSpaces(expr, index);
node = {
type: MEMBER_EXP,
computed: false,
object: node,
property: gobbleIdentifier()
};
} else if (ch_i === OBRACK_CODE) {
node = {
type: MEMBER_EXP,
computed: true,
object: node,
property: gobbleExpression()
};
index = gobbleSpaces(expr, index);
ch_i = exprICode(index);
if (ch_i !== CBRACK_CODE) {
(0, _utils.throwError)('Unclosed [', index);
}
index++;
} else if (ch_i === OPAREN_CODE) {
// A function call is being made; gobble all the arguments
node = {
type: CALL_EXP,
'arguments': gobbleArguments(CPAREN_CODE),
callee: node
};
}
index = gobbleSpaces(expr, index);
ch_i = exprICode(index);
}
return node;
},
// Responsible for parsing a group of things within parentheses `()`
// This function assumes that it needs to gobble the opening parenthesis
// and then tries to gobble everything within that parenthesis, assuming
// that the next thing it should see is the close parenthesis. If not,
// then the expression probably doesn't have a `)`
gobbleGroup = function gobbleGroup() {
index++;
var node = gobbleExpression();
index = gobbleSpaces(expr, index);
if (exprICode(index) === CPAREN_CODE) {
index++;
return node;
} else {
(0, _utils.throwError)('Unclosed (', index);
}
},
// Responsible for parsing Array literals `[1, 2, 3]`
// This function assumes that it needs to gobble the opening bracket
// and then tries to gobble the expressions as arguments.
gobbleArray = function gobbleArray() {
index++;
return {
type: ARRAY_EXP,
elements: gobbleArguments(CBRACK_CODE)
};
},
nodes = [],
ch_i,
node;
while (index < length) {
ch_i = exprICode(index);
// Expressions can be separated by semicolons, commas, or just inferred without any
// separators
if (ch_i === SEMCOL_CODE || ch_i === COMMA_CODE) {
index++; // ignore separators
} else {
// Try to gobble each expression individually
if (node = gobbleExpression()) {
nodes.push(node);
// If we weren't able to find a binary expression and are out of room, then
// the expression passed in probably has too much
} else if (index < length) {
(0, _utils.throwError)('Unexpected "' + exprI(index) + '"', index);
}
}
}
// If there's only one expression just try returning the expression
if (nodes.length === 1) {
return nodes[0];
} else {
return {
type: COMPOUND,
body: nodes
};
}
};
parser.registerUnary = function (op_name) {
max_unop_len = Math.max(op_name.length, max_unop_len);
unary_ops[op_name] = 1;
return this;
};
parser.registerBinary = function (op_name, precedence) {
max_binop_len = Math.max(op_name.length, max_binop_len);
binary_ops[op_name] = precedence;
return this;
};
parser.addLiteral = function (literal_name, literal_value) {
literals[literal_name] = literal_value;
return this;
};
parser.removeUnaryOp = function (op_name) {
delete unary_ops[op_name];
if (op_name.length === max_unop_len) {
max_unop_len = (0, _utils.getMaxKeyLen)(unary_ops);
}
return this;
};
parser.removeBinaryOp = function (op_name) {
delete binary_ops[op_name];
if (op_name.length === max_binop_len) {
max_binop_len = (0, _utils.getMaxKeyLen)(binary_ops);
}
return this;
};
parser.removeLiteral = function (literal_name) {
delete literals[literal_name];
return this;
};
//
var FAIL = {};
var execs = {};
execs[LITERAL] = function (x) {
return x.value;
};
execs[UNARY_EXP] = function (node, vars) {
var val = execWalk(node.argument, vars);
return evals[node.operator] ? evals[node.operator](val) : FAIL;
};
execs[ARRAY_EXP] = function (node, vars) {
var xs = [];
for (var i = 0, l = node.elements.length; i < l; i++) {
var x = execWalk(node.elements[i], vars);
if (x === FAIL) return FAIL;
xs.push(x);
}
return xs;
};
execs[BINARY_EXP] = execs[LOGICAL_EXP] = function (node, vars) {
var left = execWalk(node.left, vars);
if (left === FAIL) {
return FAIL;
}
var right = execWalk(node.right, vars);
if (right === FAIL) {
return FAIL;
}
var op = node.operator;
return evals[op] ? evals[op](left, right) : FAIL;
};
execs[IDENTIFIER] = function (node, vars) {
if ({}.hasOwnProperty.call(vars, node.name)) {
return vars[node.name];
} else return FAIL;
};
execs[THIS_EXP] = function (node, vars) {
if ({}.hasOwnProperty.call(vars, 'this')) {
return vars['this'];
} else return FAIL;
};
execs[CALL_EXP] = function (node, vars) {
var callee = execWalk(node.callee, vars);
if (callee === FAIL) return FAIL;
if (typeof callee !== 'function') return FAIL;
var ctx = node.callee.object ? execWalk(node.callee.object, vars) : null;
var args = [];
for (var i = 0, l = node.arguments.length; i < l; i++) {
var x = execWalk(node.arguments[i], vars);
if (x === FAIL) return FAIL;
args.push(x);
}
return callee.apply(ctx, args);
};
execs[MEMBER_EXP] = function (node, vars) {
var obj = execWalk(node.object, vars);
// if ((obj === FAIL) || (typeof obj == 'function')) {
// return FAIL
// }
if (obj === FAIL) {
return FAIL;
}
// console.log('node.property: ', node.property)
if (node.property.type === IDENTIFIER) {
return obj[vars[node.property.name] || node.property.name];
}
var prop = execWalk(node.property, vars);
if (prop === FAIL) return FAIL;
return obj[prop];
};
execs[CONDITIONAL_EXP] = function (node, vars) {
var val = execWalk(node.test, vars);
if (val === FAIL) return FAIL;
return val ? execWalk(node.consequent, vars) : execWalk(node.alternate, vars);
};
var execWalk = function execWalk(node, vars) {
var type = node.type;
if (type === UNARY_EXP) {
return execs[type](node, vars);
}
return execs[node.type] ? execs[node.type](node, vars) : FAIL;
};
function Formulax(source) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// this.binaryOps = binary
// this.unaryOps = unary
this.functions = {};
this.data = {};
if (source) {
this.source = source;
this.formula = parser(source);
}
}
Formulax.prototype = {
flatten: function flatten() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return [].concat.call([], args);
},
parse: function parse(newSource) {
this.source = newSource;
this.formula = parser(newSource);
return this;
},
setUnary: function setUnary(name, fn) {
parser.registerUnary(name);
},
setBinary: function setBinary(name, fn) {
parser.registerBinary(name);
},
setFunction: function setFunction(name, fn) {
this.functions[name] = fn;
},
eval: function _eval(source, data) {
return this.parse(source).exec(data);
},
exec: function exec(data) {
return execWalk(this.formula, data);
}
};
exports.default = Formulax;