/*global module: true, exports: true, console: true */
(function (root) {
'use strict';
JavaScript Expression Parser (JSEP) 0.3.5
JSEP may be freely distributed under the MIT License
/*global module: true, exports: true, console: true */
(function (root) {
'use strict';
This is the full set of types that any JSEP node can be. Store them here to save space when minified
var COMPOUND = 'Compound',
IDENTIFIER = 'Identifier',
MEMBER_EXP = 'MemberExpression',
LITERAL = 'Literal',
THIS_EXP = 'ThisExpression',
CALL_EXP = 'CallExpression',
UNARY_EXP = 'UnaryExpression',
BINARY_EXP = 'BinaryExpression',
LOGICAL_EXP = 'LogicalExpression',
CONDITIONAL_EXP = 'ConditionalExpression',
ARRAY_EXP = 'ArrayExpression',
PERIOD_CODE = 46, // '.'
COMMA_CODE = 44, // ','
SQUOTE_CODE = 39, // single quote
DQUOTE_CODE = 34, // double quotes
OPAREN_CODE = 40, // (
CPAREN_CODE = 41, // )
OBRACK_CODE = 91, // [
CBRACK_CODE = 93, // ]
QUMARK_CODE = 63, // ?
SEMCOL_CODE = 59, // ;
COLON_CODE = 58, // :
throwError = function(message, index) {
var error = new Error(message + ' at character ' + index);
error.index = index;
error.description = message;
throw error;
},
Set t
to true
to save space (when minified, not gzipped)
t = true,
Use a quickly-accessible map to store all of the unary operators
Values are set to true
(it really doesn’t matter)
unary_ops = {'-': t, '!': t, '~': t, '+': t},
Also use a map for the binary operations but set their values to their binary precedence for quick reference: see Order of operations
binary_ops = {
'||': 1, '&&': 2, '|': 3, '^': 4, '&': 5,
'==': 6, '!=': 6, '===': 6, '!==': 6,
'<': 7, '>': 7, '<=': 7, '>=': 7,
'<<':8, '>>': 8, '>>>': 8,
'+': 9, '-': 9,
'*': 10, '/': 10, '%': 10
},
Get return the longest key length of any object
getMaxKeyLen = function(obj) {
var max_len = 0, len;
for(var key in obj) {
if((len = key.length) > max_len && obj.hasOwnProperty(key)) {
max_len = len;
}
}
return max_len;
},
max_unop_len = getMaxKeyLen(unary_ops),
max_binop_len = getMaxKeyLen(binary_ops),
literals = {
'true': true,
'false': false,
'null': null
},
Except for this
, which is special. This could be changed to something like 'self'
as well
this_str = 'this',
Returns the precedence of a binary operator or 0
if it isn’t a binary operator
binaryPrecedence = function(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
createBinaryExpression = function (operator, left, right) {
var type = (operator === '||' || operator === '&&') ? LOGICAL_EXP : BINARY_EXP;
return {
type: type,
operator: operator,
left: left,
right: right
};
},
ch
is a character code in the next three functions
isDecimalDigit = function(ch) {
return (ch >= 48 && ch <= 57); // 0...9
},
isIdentifierStart = function(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
},
isIdentifierPart = function(ch) {
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
},
jsep = function(expr) {
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,
charAtFunc = expr.charAt,
charCodeAtFunc = expr.charCodeAt,
exprI = function(i) { return charAtFunc.call(expr, i); },
exprICode = function(i) { return charCodeAtFunc.call(expr, i); },
length = expr.length,
Push index
up to the next non-space character
gobbleSpaces = function() {
var ch = exprICode(index);
space or tab
while(ch === 32 || ch === 9 || ch === 10 || ch === 13) {
ch = exprICode(++index);
}
},
The main parsing function. Much of this code is dedicated to ternary expressions
gobbleExpression = function() {
var test = gobbleBinaryExpression(),
consequent, alternate;
gobbleSpaces();
if(exprICode(index) === QUMARK_CODE) {
Ternary expression: test ? consequent : alternate
index++;
consequent = gobbleExpression();
if(!consequent) {
throwError('Expected expression', index);
}
gobbleSpaces();
if(exprICode(index) === COLON_CODE) {
index++;
alternate = gobbleExpression();
if(!alternate) {
throwError('Expected expression', index);
}
return {
type: CONDITIONAL_EXP,
test: test,
consequent: consequent,
alternate: alternate
};
} else {
throwError('Expected :', index);
}
} else {
return test;
}
},
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
gobbleBinaryOp = function() {
gobbleSpaces();
var biop, to_check = expr.substr(index, max_binop_len), tc_len = to_check.length;
while(tc_len > 0) {
Don’t accept a binary op when it is an identifier. Binary ops that start with a identifier-valid character must be followed by a non identifier-part valid character
if(binary_ops.hasOwnProperty(to_check) && (
!isIdentifierStart(exprICode(index)) ||
(index+to_check.length< expr.length && !isIdentifierPart(exprICode(index+to_check.length)))
)) {
index += tc_len;
return to_check;
}
to_check = to_check.substr(0, --tc_len);
}
return false;
},
This function is responsible for gobbling an individual expression,
e.g. 1
, 1+2
, a+(b*2)-Math.sqrt(2)
gobbleBinaryExpression = function() {
var ch_i, 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();
If there wasn’t a binary operator, just return the leftmost node
if(!biop) {
return left;
}
Otherwise, we need to start a stack to properly place the binary operations in their precedence structure
biop_info = { value: biop, prec: binaryPrecedence(biop)};
right = gobbleToken();
if(!right) {
throwError("Expected expression after " + biop, index);
}
stack = [left, biop_info, right];
Properly deal with precedence using recursive descent
while((biop = gobbleBinaryOp())) {
prec = binaryPrecedence(biop);
if(prec === 0) {
break;
}
biop_info = { value: biop, 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) {
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() {
var ch, to_check, tc_len;
gobbleSpaces();
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) {
Don’t accept an unary op when it is an identifier. Unary ops that start with a identifier-valid character must be followed by a non identifier-part valid character
if(unary_ops.hasOwnProperty(to_check) && (
!isIdentifierStart(exprICode(index)) ||
(index+to_check.length < expr.length && !isIdentifierPart(exprICode(index+to_check.length)))
)) {
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() {
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)) ) {
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)) {
throwError('Variable names cannot start with a number (' +
number + exprI(index) + ')', index);
} else if(chCode === PERIOD_CODE) {
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\nJSEP'
gobbleStringLiteral = function() {
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) {
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() {
var ch = exprICode(index), start = index, identifier;
if(isIdentifierStart(ch)) {
index++;
} else {
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(termination) {
var ch_i, args = [], node, closed = false;
while(index < length) {
gobbleSpaces();
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) {
throwError('Expected comma', index);
}
args.push(node);
}
}
if (!closed) {
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() {
var ch_i, node;
ch_i = exprICode(index);
if(ch_i === OPAREN_CODE) {
node = gobbleGroup();
} else {
node = gobbleIdentifier();
}
gobbleSpaces();
ch_i = exprICode(index);
while(ch_i === PERIOD_CODE || ch_i === OBRACK_CODE || ch_i === OPAREN_CODE) {
index++;
if(ch_i === PERIOD_CODE) {
gobbleSpaces();
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()
};
gobbleSpaces();
ch_i = exprICode(index);
if(ch_i !== CBRACK_CODE) {
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
};
}
gobbleSpaces();
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() {
index++;
var node = gobbleExpression();
gobbleSpaces();
if(exprICode(index) === CPAREN_CODE) {
index++;
return node;
} else {
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() {
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) {
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
};
}
};
To be filled in by the template
jsep.version = '0.3.5';
jsep.toString = function() { return 'JavaScript Expression Parser (JSEP) v' + jsep.version; };
/**
* @method jsep.addUnaryOp
* @param {string} op_name The name of the unary op to add
* @return jsep
*/
jsep.addUnaryOp = function(op_name) {
max_unop_len = Math.max(op_name.length, max_unop_len);
unary_ops[op_name] = t; return this;
};
/**
* @method jsep.addBinaryOp
* @param {string} op_name The name of the binary op to add
* @param {number} precedence The precedence of the binary op (can be a float)
* @return jsep
*/
jsep.addBinaryOp = function(op_name, precedence) {
max_binop_len = Math.max(op_name.length, max_binop_len);
binary_ops[op_name] = precedence;
return this;
};
/**
* @method jsep.addLiteral
* @param {string} literal_name The name of the literal to add
* @param {*} literal_value The value of the literal
* @return jsep
*/
jsep.addLiteral = function(literal_name, literal_value) {
literals[literal_name] = literal_value;
return this;
};
/**
* @method jsep.removeUnaryOp
* @param {string} op_name The name of the unary op to remove
* @return jsep
*/
jsep.removeUnaryOp = function(op_name) {
delete unary_ops[op_name];
if(op_name.length === max_unop_len) {
max_unop_len = getMaxKeyLen(unary_ops);
}
return this;
};
/**
* @method jsep.removeAllUnaryOps
* @return jsep
*/
jsep.removeAllUnaryOps = function() {
unary_ops = {};
max_unop_len = 0;
return this;
};
/**
* @method jsep.removeBinaryOp
* @param {string} op_name The name of the binary op to remove
* @return jsep
*/
jsep.removeBinaryOp = function(op_name) {
delete binary_ops[op_name];
if(op_name.length === max_binop_len) {
max_binop_len = getMaxKeyLen(binary_ops);
}
return this;
};
/**
* @method jsep.removeAllBinaryOps
* @return jsep
*/
jsep.removeAllBinaryOps = function() {
binary_ops = {};
max_binop_len = 0;
return this;
};
/**
* @method jsep.removeLiteral
* @param {string} literal_name The name of the literal to remove
* @return jsep
*/
jsep.removeLiteral = function(literal_name) {
delete literals[literal_name];
return this;
};
/**
* @method jsep.removeAllLiterals
* @return jsep
*/
jsep.removeAllLiterals = function() {
literals = {};
return this;
};
In desktop environments, have a way to restore the old value for jsep
if (typeof exports === 'undefined') {
var old_jsep = root.jsep;
The star of the show! It’s a function!
root.jsep = jsep;
And a courteous function willing to move out of the way for other similarly-named objects!
jsep.noConflict = function() {
if(root.jsep === jsep) {
root.jsep = old_jsep;
}
return jsep;
};
} else {
In Node.JS environments
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = jsep;
} else {
exports.parse = jsep;
}
}
}(this));