UNPKG

20.8 kBJavaScriptView Raw
1// Code originally from https://github.com/soney/jsep
2// Copyright (c) 2013 Stephen Oney, http://jsep.from.so/
3// Code modified and adapted to work with d3-view
4
5// This is the full set of types that any JSEP node can be.
6// Store them here to save space when minified
7export const code = {
8 COMPOUND: "Compound",
9 IDENTIFIER: "Identifier",
10 MEMBER_EXP: "MemberExpression",
11 LITERAL: "Literal",
12 THIS_EXP: "ThisExpression",
13 CALL_EXP: "CallExpression",
14 UNARY_EXP: "UnaryExpression",
15 BINARY_EXP: "BinaryExpression",
16 LOGICAL_EXP: "LogicalExpression",
17 CONDITIONAL_EXP: "ConditionalExpression",
18 ARRAY_EXP: "ArrayExpression"
19};
20
21var PERIOD_CODE = 46, // '.'
22 COMMA_CODE = 44, // ','
23 SQUOTE_CODE = 39, // single quote
24 DQUOTE_CODE = 34, // double quotes
25 OPAREN_CODE = 40, // (
26 CPAREN_CODE = 41, // )
27 OBRACK_CODE = 91, // [
28 CBRACK_CODE = 93, // ]
29 QUMARK_CODE = 63, // ?
30 SEMCOL_CODE = 59, // ;
31 COLON_CODE = 58, // :
32 throwError = function(message, index) {
33 var error = new Error(message + " at character " + index);
34 error.index = index;
35 error.description = message;
36 throw error;
37 },
38 // Operations
39 // ----------
40
41 // Set `t` to `true` to save space (when minified, not gzipped)
42 t = true,
43 // Use a quickly-accessible map to store all of the unary operators
44 // Values are set to `true` (it really doesn't matter)
45 unary_ops = { "-": t, "!": t, "~": t, "+": t },
46 // Also use a map for the binary operations but set their values to their
47 // binary precedence for quick reference:
48 // see [Order of operations](http://en.wikipedia.org/wiki/Order_of_operations#Programming_language)
49 binary_ops = {
50 "||": 1,
51 "&&": 2,
52 "|": 3,
53 "^": 4,
54 "&": 5,
55 "==": 6,
56 "!=": 6,
57 "===": 6,
58 "!==": 6,
59 "<": 7,
60 ">": 7,
61 "<=": 7,
62 ">=": 7,
63 "<<": 8,
64 ">>": 8,
65 ">>>": 8,
66 "+": 9,
67 "-": 9,
68 "*": 10,
69 "/": 10,
70 "%": 10
71 },
72 // Get return the longest key length of any object
73 getMaxKeyLen = function(obj) {
74 var max_len = 0,
75 len;
76 for (var key in obj) {
77 if ((len = key.length) > max_len && obj.hasOwnProperty(key)) {
78 max_len = len;
79 }
80 }
81 return max_len;
82 },
83 max_unop_len = getMaxKeyLen(unary_ops),
84 max_binop_len = getMaxKeyLen(binary_ops),
85 // Literals
86 // ----------
87 // Store the values to return for the various literals we may encounter
88 literals = {
89 true: true,
90 false: false,
91 null: null
92 },
93 // Except for `this`, which is special. This could be changed to something like `'self'` as well
94 this_str = "this",
95 // Returns the precedence of a binary operator or `0` if it isn't a binary operator
96 binaryPrecedence = function(op_val) {
97 return binary_ops[op_val] || 0;
98 },
99 // Utility function (gets called from multiple places)
100 // Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions
101 createBinaryExpression = function(operator, left, right) {
102 var type =
103 operator === "||" || operator === "&&"
104 ? code.LOGICAL_EXP
105 : code.BINARY_EXP;
106 return {
107 type: type,
108 operator: operator,
109 left: left,
110 right: right
111 };
112 },
113 // `ch` is a character code in the next three functions
114 isDecimalDigit = function(ch) {
115 return ch >= 48 && ch <= 57; // 0...9
116 },
117 isIdentifierStart = function(ch) {
118 return (
119 ch === 36 ||
120 ch === 95 || // `$` and `_`
121 (ch >= 65 && ch <= 90) || // A...Z
122 (ch >= 97 && ch <= 122) || // a...z
123 (ch >= 128 && !binary_ops[String.fromCharCode(ch)])
124 ); // any non-ASCII that is not an operator
125 },
126 isIdentifierPart = function(ch) {
127 return (
128 ch === 36 ||
129 ch === 95 || // `$` and `_`
130 (ch >= 65 && ch <= 90) || // A...Z
131 (ch >= 97 && ch <= 122) || // a...z
132 (ch >= 48 && ch <= 57) || // 0...9
133 (ch >= 128 && !binary_ops[String.fromCharCode(ch)])
134 ); // any non-ASCII that is not an operator
135 },
136 // Parsing
137 // -------
138 // `expr` is a string with the passed in expression
139 jsep = function(expr) {
140 // `index` stores the character number we are currently at while `length` is a constant
141 // All of the gobbles below will modify `index` as we move along
142 var index = 0,
143 charAtFunc = expr.charAt,
144 charCodeAtFunc = expr.charCodeAt,
145 exprI = function(i) {
146 return charAtFunc.call(expr, i);
147 },
148 exprICode = function(i) {
149 return charCodeAtFunc.call(expr, i);
150 },
151 length = expr.length,
152 // Push `index` up to the next non-space character
153 gobbleSpaces = function() {
154 var ch = exprICode(index);
155 // space or tab
156 while (ch === 32 || ch === 9) {
157 ch = exprICode(++index);
158 }
159 },
160 // The main parsing function. Much of this code is dedicated to ternary expressions
161 gobbleExpression = function() {
162 var test = gobbleBinaryExpression(),
163 consequent,
164 alternate;
165 gobbleSpaces();
166 if (exprICode(index) === QUMARK_CODE) {
167 // Ternary expression: test ? consequent : alternate
168 index++;
169 consequent = gobbleExpression();
170 if (!consequent) {
171 throwError("Expected expression", index);
172 }
173 gobbleSpaces();
174 if (exprICode(index) === COLON_CODE) {
175 index++;
176 alternate = gobbleExpression();
177 if (!alternate) {
178 throwError("Expected expression", index);
179 }
180 return {
181 type: code.CONDITIONAL_EXP,
182 test: test,
183 consequent: consequent,
184 alternate: alternate
185 };
186 } else {
187 throwError("Expected :", index);
188 }
189 } else {
190 return test;
191 }
192 },
193 // Search for the operation portion of the string (e.g. `+`, `===`)
194 // Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
195 // and move down from 3 to 2 to 1 character until a matching binary operation is found
196 // then, return that binary operation
197 gobbleBinaryOp = function() {
198 gobbleSpaces();
199 var to_check = expr.substr(index, max_binop_len),
200 tc_len = to_check.length;
201 while (tc_len > 0) {
202 if (binary_ops.hasOwnProperty(to_check)) {
203 index += tc_len;
204 return to_check;
205 }
206 to_check = to_check.substr(0, --tc_len);
207 }
208 return false;
209 },
210 // This function is responsible for gobbling an individual expression,
211 // e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
212 gobbleBinaryExpression = function() {
213 var node, biop, prec, stack, biop_info, left, right, i;
214
215 // First, try to get the leftmost thing
216 // Then, check to see if there's a binary operator operating on that leftmost thing
217 left = gobbleToken();
218 biop = gobbleBinaryOp();
219
220 // If there wasn't a binary operator, just return the leftmost node
221 if (!biop) {
222 return left;
223 }
224
225 // Otherwise, we need to start a stack to properly place the binary operations in their
226 // precedence structure
227 biop_info = { value: biop, prec: binaryPrecedence(biop) };
228
229 right = gobbleToken();
230 if (!right) {
231 throwError("Expected expression after " + biop, index);
232 }
233 stack = [left, biop_info, right];
234
235 // Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
236 while ((biop = gobbleBinaryOp())) {
237 prec = binaryPrecedence(biop);
238
239 if (prec === 0) {
240 break;
241 }
242 biop_info = { value: biop, prec: prec };
243
244 // Reduce: make a binary expression from the three topmost entries.
245 while (stack.length > 2 && prec <= stack[stack.length - 2].prec) {
246 right = stack.pop();
247 biop = stack.pop().value;
248 left = stack.pop();
249 node = createBinaryExpression(biop, left, right);
250 stack.push(node);
251 }
252
253 node = gobbleToken();
254 if (!node) {
255 throwError("Expected expression after " + biop, index);
256 }
257 stack.push(biop_info, node);
258 }
259
260 i = stack.length - 1;
261 node = stack[i];
262 while (i > 1) {
263 node = createBinaryExpression(stack[i - 1].value, stack[i - 2], node);
264 i -= 2;
265 }
266 return node;
267 },
268 // An individual part of a binary expression:
269 // e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
270 gobbleToken = function() {
271 var ch, to_check, tc_len;
272
273 gobbleSpaces();
274 ch = exprICode(index);
275
276 if (isDecimalDigit(ch) || ch === PERIOD_CODE) {
277 // Char code 46 is a dot `.` which can start off a numeric literal
278 return gobbleNumericLiteral();
279 } else if (ch === SQUOTE_CODE || ch === DQUOTE_CODE) {
280 // Single or double quotes
281 return gobbleStringLiteral();
282 } else if (isIdentifierStart(ch) || ch === OPAREN_CODE) {
283 // open parenthesis
284 // `foo`, `bar.baz`
285 return gobbleVariable();
286 } else if (ch === OBRACK_CODE) {
287 return gobbleArray();
288 } else {
289 to_check = expr.substr(index, max_unop_len);
290 tc_len = to_check.length;
291 while (tc_len > 0) {
292 if (unary_ops.hasOwnProperty(to_check)) {
293 index += tc_len;
294 return {
295 type: code.UNARY_EXP,
296 operator: to_check,
297 argument: gobbleToken(),
298 prefix: true
299 };
300 }
301 to_check = to_check.substr(0, --tc_len);
302 }
303
304 return false;
305 }
306 },
307 // Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
308 // keep track of everything in the numeric literal and then calling `parseFloat` on that string
309 gobbleNumericLiteral = function() {
310 var number = "",
311 ch,
312 chCode;
313 while (isDecimalDigit(exprICode(index))) {
314 number += exprI(index++);
315 }
316
317 if (exprICode(index) === PERIOD_CODE) {
318 // can start with a decimal marker
319 number += exprI(index++);
320
321 while (isDecimalDigit(exprICode(index))) {
322 number += exprI(index++);
323 }
324 }
325
326 ch = exprI(index);
327 if (ch === "e" || ch === "E") {
328 // exponent marker
329 number += exprI(index++);
330 ch = exprI(index);
331 if (ch === "+" || ch === "-") {
332 // exponent sign
333 number += exprI(index++);
334 }
335 while (isDecimalDigit(exprICode(index))) {
336 //exponent itself
337 number += exprI(index++);
338 }
339 if (!isDecimalDigit(exprICode(index - 1))) {
340 throwError(
341 "Expected exponent (" + number + exprI(index) + ")",
342 index
343 );
344 }
345 }
346
347 chCode = exprICode(index);
348 // Check to make sure this isn't a variable name that start with a number (123abc)
349 if (isIdentifierStart(chCode)) {
350 throwError(
351 "Variable names cannot start with a number (" +
352 number +
353 exprI(index) +
354 ")",
355 index
356 );
357 } else if (chCode === PERIOD_CODE) {
358 throwError("Unexpected period", index);
359 }
360
361 return {
362 type: code.LITERAL,
363 value: parseFloat(number),
364 raw: number
365 };
366 },
367 // Parses a string literal, staring with single or double quotes with basic support for escape codes
368 // e.g. `"hello world"`, `'this is\nJSEP'`
369 gobbleStringLiteral = function() {
370 var str = "",
371 quote = exprI(index++),
372 closed = false,
373 ch;
374
375 while (index < length) {
376 ch = exprI(index++);
377 if (ch === quote) {
378 closed = true;
379 break;
380 } else if (ch === "\\") {
381 // Check for all of the common escape codes
382 ch = exprI(index++);
383 switch (ch) {
384 case "n":
385 str += "\n";
386 break;
387 case "r":
388 str += "\r";
389 break;
390 case "t":
391 str += "\t";
392 break;
393 case "b":
394 str += "\b";
395 break;
396 case "f":
397 str += "\f";
398 break;
399 case "v":
400 str += "\x0B";
401 break;
402 default:
403 str += "\\" + ch;
404 }
405 } else {
406 str += ch;
407 }
408 }
409
410 if (!closed) {
411 throwError('Unclosed quote after "' + str + '"', index);
412 }
413
414 return {
415 type: code.LITERAL,
416 value: str,
417 raw: quote + str + quote
418 };
419 },
420 // Gobbles only identifiers
421 // e.g.: `foo`, `_value`, `$x1`
422 // Also, this function checks if that identifier is a literal:
423 // (e.g. `true`, `false`, `null`) or `this`
424 gobbleIdentifier = function() {
425 var ch = exprICode(index),
426 start = index,
427 identifier;
428
429 if (isIdentifierStart(ch)) {
430 index++;
431 } else {
432 throwError("Unexpected " + exprI(index), index);
433 }
434
435 while (index < length) {
436 ch = exprICode(index);
437 if (isIdentifierPart(ch)) {
438 index++;
439 } else {
440 break;
441 }
442 }
443 identifier = expr.slice(start, index);
444
445 if (literals.hasOwnProperty(identifier)) {
446 return {
447 type: code.LITERAL,
448 value: literals[identifier],
449 raw: identifier
450 };
451 } else if (identifier === this_str) {
452 return { type: code.THIS_EXP };
453 } else {
454 return {
455 type: code.IDENTIFIER,
456 name: identifier
457 };
458 }
459 },
460 // Gobbles a list of arguments within the context of a function call
461 // or array literal. This function also assumes that the opening character
462 // `(` or `[` has already been gobbled, and gobbles expressions and commas
463 // until the terminator character `)` or `]` is encountered.
464 // e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
465 gobbleArguments = function(termination) {
466 var ch_i,
467 args = [],
468 node,
469 closed = false;
470 while (index < length) {
471 gobbleSpaces();
472 ch_i = exprICode(index);
473 if (ch_i === termination) {
474 // done parsing
475 closed = true;
476 index++;
477 break;
478 } else if (ch_i === COMMA_CODE) {
479 // between expressions
480 index++;
481 } else {
482 node = gobbleExpression();
483 if (!node || node.type === code.COMPOUND) {
484 throwError("Expected comma", index);
485 }
486 args.push(node);
487 }
488 }
489 if (!closed) {
490 throwError("Expected " + String.fromCharCode(termination), index);
491 }
492 return args;
493 },
494 // Gobble a non-literal variable name. This variable name may include properties
495 // e.g. `foo`, `bar.baz`, `foo['bar'].baz`
496 // It also gobbles function calls:
497 // e.g. `Math.acos(obj.angle)`
498 gobbleVariable = function() {
499 var ch_i, node;
500 ch_i = exprICode(index);
501
502 if (ch_i === OPAREN_CODE) {
503 node = gobbleGroup();
504 } else {
505 node = gobbleIdentifier();
506 }
507 gobbleSpaces();
508 ch_i = exprICode(index);
509 while (
510 ch_i === PERIOD_CODE ||
511 ch_i === OBRACK_CODE ||
512 ch_i === OPAREN_CODE
513 ) {
514 index++;
515 if (ch_i === PERIOD_CODE) {
516 gobbleSpaces();
517 node = {
518 type: code.MEMBER_EXP,
519 computed: false,
520 object: node,
521 property: gobbleIdentifier()
522 };
523 } else if (ch_i === OBRACK_CODE) {
524 node = {
525 type: code.MEMBER_EXP,
526 computed: true,
527 object: node,
528 property: gobbleExpression()
529 };
530 gobbleSpaces();
531 ch_i = exprICode(index);
532 if (ch_i !== CBRACK_CODE) {
533 throwError("Unclosed [", index);
534 }
535 index++;
536 } else if (ch_i === OPAREN_CODE) {
537 // A function call is being made; gobble all the arguments
538 node = {
539 type: code.CALL_EXP,
540 arguments: gobbleArguments(CPAREN_CODE),
541 callee: node
542 };
543 }
544 gobbleSpaces();
545 ch_i = exprICode(index);
546 }
547 return node;
548 },
549 // Responsible for parsing a group of things within parentheses `()`
550 // This function assumes that it needs to gobble the opening parenthesis
551 // and then tries to gobble everything within that parenthesis, assuming
552 // that the next thing it should see is the close parenthesis. If not,
553 // then the expression probably doesn't have a `)`
554 gobbleGroup = function() {
555 index++;
556 var node = gobbleExpression();
557 gobbleSpaces();
558 if (exprICode(index) === CPAREN_CODE) {
559 index++;
560 return node;
561 } else {
562 throwError("Unclosed (", index);
563 }
564 },
565 // Responsible for parsing Array literals `[1, 2, 3]`
566 // This function assumes that it needs to gobble the opening bracket
567 // and then tries to gobble the expressions as arguments.
568 gobbleArray = function() {
569 index++;
570 return {
571 type: code.ARRAY_EXP,
572 elements: gobbleArguments(CBRACK_CODE)
573 };
574 },
575 nodes = [],
576 ch_i,
577 node;
578
579 while (index < length) {
580 ch_i = exprICode(index);
581
582 // Expressions can be separated by semicolons, commas, or just inferred without any
583 // separators
584 if (ch_i === SEMCOL_CODE || ch_i === COMMA_CODE) {
585 index++; // ignore separators
586 } else {
587 // Try to gobble each expression individually
588 if ((node = gobbleExpression())) {
589 nodes.push(node);
590 // If we weren't able to find a binary expression and are out of room, then
591 // the expression passed in probably has too much
592 } else if (index < length) {
593 throwError('Unexpected "' + exprI(index) + '"', index);
594 }
595 }
596 }
597
598 // If there's only one expression just try returning the expression
599 if (nodes.length === 1) {
600 return nodes[0];
601 } else {
602 return {
603 type: code.COMPOUND,
604 body: nodes
605 };
606 }
607 };
608
609/**
610 * @method jsep.addUnaryOp
611 * @param {string} op_name The name of the unary op to add
612 * @return jsep
613 */
614jsep.addUnaryOp = function(op_name) {
615 max_unop_len = Math.max(op_name.length, max_unop_len);
616 unary_ops[op_name] = t;
617 return this;
618};
619
620/**
621 * @method jsep.addBinaryOp
622 * @param {string} op_name The name of the binary op to add
623 * @param {number} precedence The precedence of the binary op (can be a float)
624 * @return jsep
625 */
626jsep.addBinaryOp = function(op_name, precedence) {
627 max_binop_len = Math.max(op_name.length, max_binop_len);
628 binary_ops[op_name] = precedence;
629 return this;
630};
631
632/**
633 * @method jsep.addLiteral
634 * @param {string} literal_name The name of the literal to add
635 * @param {*} literal_value The value of the literal
636 * @return jsep
637 */
638jsep.addLiteral = function(literal_name, literal_value) {
639 literals[literal_name] = literal_value;
640 return this;
641};
642
643/**
644 * @method jsep.removeUnaryOp
645 * @param {string} op_name The name of the unary op to remove
646 * @return jsep
647 */
648jsep.removeUnaryOp = function(op_name) {
649 delete unary_ops[op_name];
650 if (op_name.length === max_unop_len) {
651 max_unop_len = getMaxKeyLen(unary_ops);
652 }
653 return this;
654};
655
656/**
657 * @method jsep.removeBinaryOp
658 * @param {string} op_name The name of the binary op to remove
659 * @return jsep
660 */
661jsep.removeBinaryOp = function(op_name) {
662 delete binary_ops[op_name];
663 if (op_name.length === max_binop_len) {
664 max_binop_len = getMaxKeyLen(binary_ops);
665 }
666 return this;
667};
668
669/**
670 * @method jsep.removeLiteral
671 * @param {string} literal_name The name of the literal to remove
672 * @return jsep
673 */
674jsep.removeLiteral = function(literal_name) {
675 delete literals[literal_name];
676 return this;
677};
678
679export default jsep;