d3-view
Version:
View-model d3 plugin for building interactive data-driven web interfaces
680 lines (642 loc) • 20.8 kB
JavaScript
// Code originally from https://github.com/soney/jsep
// Copyright (c) 2013 Stephen Oney, http://jsep.from.so/
// Code modified and adapted to work with d3-view
// This is the full set of types that any JSEP node can be.
// Store them here to save space when minified
export const code = {
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"
};
var 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;
},
// Operations
// ----------
// 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](http://en.wikipedia.org/wiki/Order_of_operations#Programming_language)
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
// ----------
// Store the values to return for the various literals we may encounter
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 === "&&"
? code.LOGICAL_EXP
: code.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
},
// Parsing
// -------
// `expr` is a string with the passed in expression
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 = 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: code.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 to_check = expr.substr(index, max_binop_len),
tc_len = to_check.length;
while (tc_len > 0) {
if (binary_ops.hasOwnProperty(to_check)) {
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 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](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
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 (isIdentifierStart(ch) || ch === OPAREN_CODE) {
// open parenthesis
// `foo`, `bar.baz`
return gobbleVariable();
} 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: code.UNARY_EXP,
operator: to_check,
argument: gobbleToken(),
prefix: true
};
}
to_check = to_check.substr(0, --tc_len);
}
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: code.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: code.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: code.LITERAL,
value: literals[identifier],
raw: identifier
};
} else if (identifier === this_str) {
return { type: code.THIS_EXP };
} else {
return {
type: code.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 === code.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: code.MEMBER_EXP,
computed: false,
object: node,
property: gobbleIdentifier()
};
} else if (ch_i === OBRACK_CODE) {
node = {
type: code.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: code.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: code.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: code.COMPOUND,
body: nodes
};
}
};
/**
* @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.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.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;
};
export default jsep;