jsep.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. /**
  2. * @implements {IHooks}
  3. */
  4. class Hooks {
  5. /**
  6. * @callback HookCallback
  7. * @this {*|Jsep} this
  8. * @param {Jsep} env
  9. * @returns: void
  10. */
  11. /**
  12. * Adds the given callback to the list of callbacks for the given hook.
  13. *
  14. * The callback will be invoked when the hook it is registered for is run.
  15. *
  16. * One callback function can be registered to multiple hooks and the same hook multiple times.
  17. *
  18. * @param {string|object} name The name of the hook, or an object of callbacks keyed by name
  19. * @param {HookCallback|boolean} callback The callback function which is given environment variables.
  20. * @param {?boolean} [first=false] Will add the hook to the top of the list (defaults to the bottom)
  21. * @public
  22. */
  23. add(name, callback, first) {
  24. if (typeof arguments[0] != 'string') {
  25. // Multiple hook callbacks, keyed by name
  26. for (let name in arguments[0]) {
  27. this.add(name, arguments[0][name], arguments[1]);
  28. }
  29. }
  30. else {
  31. (Array.isArray(name) ? name : [name]).forEach(function (name) {
  32. this[name] = this[name] || [];
  33. if (callback) {
  34. this[name][first ? 'unshift' : 'push'](callback);
  35. }
  36. }, this);
  37. }
  38. }
  39. /**
  40. * Runs a hook invoking all registered callbacks with the given environment variables.
  41. *
  42. * Callbacks will be invoked synchronously and in the order in which they were registered.
  43. *
  44. * @param {string} name The name of the hook.
  45. * @param {Object<string, any>} env The environment variables of the hook passed to all callbacks registered.
  46. * @public
  47. */
  48. run(name, env) {
  49. this[name] = this[name] || [];
  50. this[name].forEach(function (callback) {
  51. callback.call(env && env.context ? env.context : env, env);
  52. });
  53. }
  54. }
  55. /**
  56. * @implements {IPlugins}
  57. */
  58. class Plugins {
  59. constructor(jsep) {
  60. this.jsep = jsep;
  61. this.registered = {};
  62. }
  63. /**
  64. * @callback PluginSetup
  65. * @this {Jsep} jsep
  66. * @returns: void
  67. */
  68. /**
  69. * Adds the given plugin(s) to the registry
  70. *
  71. * @param {object} plugins
  72. * @param {string} plugins.name The name of the plugin
  73. * @param {PluginSetup} plugins.init The init function
  74. * @public
  75. */
  76. register(...plugins) {
  77. plugins.forEach((plugin) => {
  78. if (typeof plugin !== 'object' || !plugin.name || !plugin.init) {
  79. throw new Error('Invalid JSEP plugin format');
  80. }
  81. if (this.registered[plugin.name]) {
  82. // already registered. Ignore.
  83. return;
  84. }
  85. plugin.init(this.jsep);
  86. this.registered[plugin.name] = plugin;
  87. });
  88. }
  89. }
  90. // JavaScript Expression Parser (JSEP) 1.3.8
  91. class Jsep {
  92. /**
  93. * @returns {string}
  94. */
  95. static get version() {
  96. // To be filled in by the template
  97. return '1.3.8';
  98. }
  99. /**
  100. * @returns {string}
  101. */
  102. static toString() {
  103. return 'JavaScript Expression Parser (JSEP) v' + Jsep.version;
  104. };
  105. // ==================== CONFIG ================================
  106. /**
  107. * @method addUnaryOp
  108. * @param {string} op_name The name of the unary op to add
  109. * @returns {Jsep}
  110. */
  111. static addUnaryOp(op_name) {
  112. Jsep.max_unop_len = Math.max(op_name.length, Jsep.max_unop_len);
  113. Jsep.unary_ops[op_name] = 1;
  114. return Jsep;
  115. }
  116. /**
  117. * @method jsep.addBinaryOp
  118. * @param {string} op_name The name of the binary op to add
  119. * @param {number} precedence The precedence of the binary op (can be a float). Higher number = higher precedence
  120. * @param {boolean} [isRightAssociative=false] whether operator is right-associative
  121. * @returns {Jsep}
  122. */
  123. static addBinaryOp(op_name, precedence, isRightAssociative) {
  124. Jsep.max_binop_len = Math.max(op_name.length, Jsep.max_binop_len);
  125. Jsep.binary_ops[op_name] = precedence;
  126. if (isRightAssociative) {
  127. Jsep.right_associative.add(op_name);
  128. }
  129. else {
  130. Jsep.right_associative.delete(op_name);
  131. }
  132. return Jsep;
  133. }
  134. /**
  135. * @method addIdentifierChar
  136. * @param {string} char The additional character to treat as a valid part of an identifier
  137. * @returns {Jsep}
  138. */
  139. static addIdentifierChar(char) {
  140. Jsep.additional_identifier_chars.add(char);
  141. return Jsep;
  142. }
  143. /**
  144. * @method addLiteral
  145. * @param {string} literal_name The name of the literal to add
  146. * @param {*} literal_value The value of the literal
  147. * @returns {Jsep}
  148. */
  149. static addLiteral(literal_name, literal_value) {
  150. Jsep.literals[literal_name] = literal_value;
  151. return Jsep;
  152. }
  153. /**
  154. * @method removeUnaryOp
  155. * @param {string} op_name The name of the unary op to remove
  156. * @returns {Jsep}
  157. */
  158. static removeUnaryOp(op_name) {
  159. delete Jsep.unary_ops[op_name];
  160. if (op_name.length === Jsep.max_unop_len) {
  161. Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
  162. }
  163. return Jsep;
  164. }
  165. /**
  166. * @method removeAllUnaryOps
  167. * @returns {Jsep}
  168. */
  169. static removeAllUnaryOps() {
  170. Jsep.unary_ops = {};
  171. Jsep.max_unop_len = 0;
  172. return Jsep;
  173. }
  174. /**
  175. * @method removeIdentifierChar
  176. * @param {string} char The additional character to stop treating as a valid part of an identifier
  177. * @returns {Jsep}
  178. */
  179. static removeIdentifierChar(char) {
  180. Jsep.additional_identifier_chars.delete(char);
  181. return Jsep;
  182. }
  183. /**
  184. * @method removeBinaryOp
  185. * @param {string} op_name The name of the binary op to remove
  186. * @returns {Jsep}
  187. */
  188. static removeBinaryOp(op_name) {
  189. delete Jsep.binary_ops[op_name];
  190. if (op_name.length === Jsep.max_binop_len) {
  191. Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
  192. }
  193. Jsep.right_associative.delete(op_name);
  194. return Jsep;
  195. }
  196. /**
  197. * @method removeAllBinaryOps
  198. * @returns {Jsep}
  199. */
  200. static removeAllBinaryOps() {
  201. Jsep.binary_ops = {};
  202. Jsep.max_binop_len = 0;
  203. return Jsep;
  204. }
  205. /**
  206. * @method removeLiteral
  207. * @param {string} literal_name The name of the literal to remove
  208. * @returns {Jsep}
  209. */
  210. static removeLiteral(literal_name) {
  211. delete Jsep.literals[literal_name];
  212. return Jsep;
  213. }
  214. /**
  215. * @method removeAllLiterals
  216. * @returns {Jsep}
  217. */
  218. static removeAllLiterals() {
  219. Jsep.literals = {};
  220. return Jsep;
  221. }
  222. // ==================== END CONFIG ============================
  223. /**
  224. * @returns {string}
  225. */
  226. get char() {
  227. return this.expr.charAt(this.index);
  228. }
  229. /**
  230. * @returns {number}
  231. */
  232. get code() {
  233. return this.expr.charCodeAt(this.index);
  234. };
  235. /**
  236. * @param {string} expr a string with the passed in express
  237. * @returns Jsep
  238. */
  239. constructor(expr) {
  240. // `index` stores the character number we are currently at
  241. // All of the gobbles below will modify `index` as we move along
  242. this.expr = expr;
  243. this.index = 0;
  244. }
  245. /**
  246. * static top-level parser
  247. * @returns {jsep.Expression}
  248. */
  249. static parse(expr) {
  250. return (new Jsep(expr)).parse();
  251. }
  252. /**
  253. * Get the longest key length of any object
  254. * @param {object} obj
  255. * @returns {number}
  256. */
  257. static getMaxKeyLen(obj) {
  258. return Math.max(0, ...Object.keys(obj).map(k => k.length));
  259. }
  260. /**
  261. * `ch` is a character code in the next three functions
  262. * @param {number} ch
  263. * @returns {boolean}
  264. */
  265. static isDecimalDigit(ch) {
  266. return (ch >= 48 && ch <= 57); // 0...9
  267. }
  268. /**
  269. * Returns the precedence of a binary operator or `0` if it isn't a binary operator. Can be float.
  270. * @param {string} op_val
  271. * @returns {number}
  272. */
  273. static binaryPrecedence(op_val) {
  274. return Jsep.binary_ops[op_val] || 0;
  275. }
  276. /**
  277. * Looks for start of identifier
  278. * @param {number} ch
  279. * @returns {boolean}
  280. */
  281. static isIdentifierStart(ch) {
  282. return (ch >= 65 && ch <= 90) || // A...Z
  283. (ch >= 97 && ch <= 122) || // a...z
  284. (ch >= 128 && !Jsep.binary_ops[String.fromCharCode(ch)]) || // any non-ASCII that is not an operator
  285. (Jsep.additional_identifier_chars.has(String.fromCharCode(ch))); // additional characters
  286. }
  287. /**
  288. * @param {number} ch
  289. * @returns {boolean}
  290. */
  291. static isIdentifierPart(ch) {
  292. return Jsep.isIdentifierStart(ch) || Jsep.isDecimalDigit(ch);
  293. }
  294. /**
  295. * throw error at index of the expression
  296. * @param {string} message
  297. * @throws
  298. */
  299. throwError(message) {
  300. const error = new Error(message + ' at character ' + this.index);
  301. error.index = this.index;
  302. error.description = message;
  303. throw error;
  304. }
  305. /**
  306. * Run a given hook
  307. * @param {string} name
  308. * @param {jsep.Expression|false} [node]
  309. * @returns {?jsep.Expression}
  310. */
  311. runHook(name, node) {
  312. if (Jsep.hooks[name]) {
  313. const env = { context: this, node };
  314. Jsep.hooks.run(name, env);
  315. return env.node;
  316. }
  317. return node;
  318. }
  319. /**
  320. * Runs a given hook until one returns a node
  321. * @param {string} name
  322. * @returns {?jsep.Expression}
  323. */
  324. searchHook(name) {
  325. if (Jsep.hooks[name]) {
  326. const env = { context: this };
  327. Jsep.hooks[name].find(function (callback) {
  328. callback.call(env.context, env);
  329. return env.node;
  330. });
  331. return env.node;
  332. }
  333. }
  334. /**
  335. * Push `index` up to the next non-space character
  336. */
  337. gobbleSpaces() {
  338. let ch = this.code;
  339. // Whitespace
  340. while (ch === Jsep.SPACE_CODE
  341. || ch === Jsep.TAB_CODE
  342. || ch === Jsep.LF_CODE
  343. || ch === Jsep.CR_CODE) {
  344. ch = this.expr.charCodeAt(++this.index);
  345. }
  346. this.runHook('gobble-spaces');
  347. }
  348. /**
  349. * Top-level method to parse all expressions and returns compound or single node
  350. * @returns {jsep.Expression}
  351. */
  352. parse() {
  353. this.runHook('before-all');
  354. const nodes = this.gobbleExpressions();
  355. // If there's only one expression just try returning the expression
  356. const node = nodes.length === 1
  357. ? nodes[0]
  358. : {
  359. type: Jsep.COMPOUND,
  360. body: nodes
  361. };
  362. return this.runHook('after-all', node);
  363. }
  364. /**
  365. * top-level parser (but can be reused within as well)
  366. * @param {number} [untilICode]
  367. * @returns {jsep.Expression[]}
  368. */
  369. gobbleExpressions(untilICode) {
  370. let nodes = [], ch_i, node;
  371. while (this.index < this.expr.length) {
  372. ch_i = this.code;
  373. // Expressions can be separated by semicolons, commas, or just inferred without any
  374. // separators
  375. if (ch_i === Jsep.SEMCOL_CODE || ch_i === Jsep.COMMA_CODE) {
  376. this.index++; // ignore separators
  377. }
  378. else {
  379. // Try to gobble each expression individually
  380. if (node = this.gobbleExpression()) {
  381. nodes.push(node);
  382. // If we weren't able to find a binary expression and are out of room, then
  383. // the expression passed in probably has too much
  384. }
  385. else if (this.index < this.expr.length) {
  386. if (ch_i === untilICode) {
  387. break;
  388. }
  389. this.throwError('Unexpected "' + this.char + '"');
  390. }
  391. }
  392. }
  393. return nodes;
  394. }
  395. /**
  396. * The main parsing function.
  397. * @returns {?jsep.Expression}
  398. */
  399. gobbleExpression() {
  400. const node = this.searchHook('gobble-expression') || this.gobbleBinaryExpression();
  401. this.gobbleSpaces();
  402. return this.runHook('after-expression', node);
  403. }
  404. /**
  405. * Search for the operation portion of the string (e.g. `+`, `===`)
  406. * Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
  407. * and move down from 3 to 2 to 1 character until a matching binary operation is found
  408. * then, return that binary operation
  409. * @returns {string|boolean}
  410. */
  411. gobbleBinaryOp() {
  412. this.gobbleSpaces();
  413. let to_check = this.expr.substr(this.index, Jsep.max_binop_len);
  414. let tc_len = to_check.length;
  415. while (tc_len > 0) {
  416. // Don't accept a binary op when it is an identifier.
  417. // Binary ops that start with a identifier-valid character must be followed
  418. // by a non identifier-part valid character
  419. if (Jsep.binary_ops.hasOwnProperty(to_check) && (
  420. !Jsep.isIdentifierStart(this.code) ||
  421. (this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))
  422. )) {
  423. this.index += tc_len;
  424. return to_check;
  425. }
  426. to_check = to_check.substr(0, --tc_len);
  427. }
  428. return false;
  429. }
  430. /**
  431. * This function is responsible for gobbling an individual expression,
  432. * e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
  433. * @returns {?jsep.BinaryExpression}
  434. */
  435. gobbleBinaryExpression() {
  436. let node, biop, prec, stack, biop_info, left, right, i, cur_biop;
  437. // First, try to get the leftmost thing
  438. // Then, check to see if there's a binary operator operating on that leftmost thing
  439. // Don't gobbleBinaryOp without a left-hand-side
  440. left = this.gobbleToken();
  441. if (!left) {
  442. return left;
  443. }
  444. biop = this.gobbleBinaryOp();
  445. // If there wasn't a binary operator, just return the leftmost node
  446. if (!biop) {
  447. return left;
  448. }
  449. // Otherwise, we need to start a stack to properly place the binary operations in their
  450. // precedence structure
  451. biop_info = { value: biop, prec: Jsep.binaryPrecedence(biop), right_a: Jsep.right_associative.has(biop) };
  452. right = this.gobbleToken();
  453. if (!right) {
  454. this.throwError("Expected expression after " + biop);
  455. }
  456. stack = [left, biop_info, right];
  457. // Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
  458. while ((biop = this.gobbleBinaryOp())) {
  459. prec = Jsep.binaryPrecedence(biop);
  460. if (prec === 0) {
  461. this.index -= biop.length;
  462. break;
  463. }
  464. biop_info = { value: biop, prec, right_a: Jsep.right_associative.has(biop) };
  465. cur_biop = biop;
  466. // Reduce: make a binary expression from the three topmost entries.
  467. const comparePrev = prev => biop_info.right_a && prev.right_a
  468. ? prec > prev.prec
  469. : prec <= prev.prec;
  470. while ((stack.length > 2) && comparePrev(stack[stack.length - 2])) {
  471. right = stack.pop();
  472. biop = stack.pop().value;
  473. left = stack.pop();
  474. node = {
  475. type: Jsep.BINARY_EXP,
  476. operator: biop,
  477. left,
  478. right
  479. };
  480. stack.push(node);
  481. }
  482. node = this.gobbleToken();
  483. if (!node) {
  484. this.throwError("Expected expression after " + cur_biop);
  485. }
  486. stack.push(biop_info, node);
  487. }
  488. i = stack.length - 1;
  489. node = stack[i];
  490. while (i > 1) {
  491. node = {
  492. type: Jsep.BINARY_EXP,
  493. operator: stack[i - 1].value,
  494. left: stack[i - 2],
  495. right: node
  496. };
  497. i -= 2;
  498. }
  499. return node;
  500. }
  501. /**
  502. * An individual part of a binary expression:
  503. * e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
  504. * @returns {boolean|jsep.Expression}
  505. */
  506. gobbleToken() {
  507. let ch, to_check, tc_len, node;
  508. this.gobbleSpaces();
  509. node = this.searchHook('gobble-token');
  510. if (node) {
  511. return this.runHook('after-token', node);
  512. }
  513. ch = this.code;
  514. if (Jsep.isDecimalDigit(ch) || ch === Jsep.PERIOD_CODE) {
  515. // Char code 46 is a dot `.` which can start off a numeric literal
  516. return this.gobbleNumericLiteral();
  517. }
  518. if (ch === Jsep.SQUOTE_CODE || ch === Jsep.DQUOTE_CODE) {
  519. // Single or double quotes
  520. node = this.gobbleStringLiteral();
  521. }
  522. else if (ch === Jsep.OBRACK_CODE) {
  523. node = this.gobbleArray();
  524. }
  525. else {
  526. to_check = this.expr.substr(this.index, Jsep.max_unop_len);
  527. tc_len = to_check.length;
  528. while (tc_len > 0) {
  529. // Don't accept an unary op when it is an identifier.
  530. // Unary ops that start with a identifier-valid character must be followed
  531. // by a non identifier-part valid character
  532. if (Jsep.unary_ops.hasOwnProperty(to_check) && (
  533. !Jsep.isIdentifierStart(this.code) ||
  534. (this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))
  535. )) {
  536. this.index += tc_len;
  537. const argument = this.gobbleToken();
  538. if (!argument) {
  539. this.throwError('missing unaryOp argument');
  540. }
  541. return this.runHook('after-token', {
  542. type: Jsep.UNARY_EXP,
  543. operator: to_check,
  544. argument,
  545. prefix: true
  546. });
  547. }
  548. to_check = to_check.substr(0, --tc_len);
  549. }
  550. if (Jsep.isIdentifierStart(ch)) {
  551. node = this.gobbleIdentifier();
  552. if (Jsep.literals.hasOwnProperty(node.name)) {
  553. node = {
  554. type: Jsep.LITERAL,
  555. value: Jsep.literals[node.name],
  556. raw: node.name,
  557. };
  558. }
  559. else if (node.name === Jsep.this_str) {
  560. node = { type: Jsep.THIS_EXP };
  561. }
  562. }
  563. else if (ch === Jsep.OPAREN_CODE) { // open parenthesis
  564. node = this.gobbleGroup();
  565. }
  566. }
  567. if (!node) {
  568. return this.runHook('after-token', false);
  569. }
  570. node = this.gobbleTokenProperty(node);
  571. return this.runHook('after-token', node);
  572. }
  573. /**
  574. * Gobble properties of of identifiers/strings/arrays/groups.
  575. * e.g. `foo`, `bar.baz`, `foo['bar'].baz`
  576. * It also gobbles function calls:
  577. * e.g. `Math.acos(obj.angle)`
  578. * @param {jsep.Expression} node
  579. * @returns {jsep.Expression}
  580. */
  581. gobbleTokenProperty(node) {
  582. this.gobbleSpaces();
  583. let ch = this.code;
  584. while (ch === Jsep.PERIOD_CODE || ch === Jsep.OBRACK_CODE || ch === Jsep.OPAREN_CODE || ch === Jsep.QUMARK_CODE) {
  585. let optional;
  586. if (ch === Jsep.QUMARK_CODE) {
  587. if (this.expr.charCodeAt(this.index + 1) !== Jsep.PERIOD_CODE) {
  588. break;
  589. }
  590. optional = true;
  591. this.index += 2;
  592. this.gobbleSpaces();
  593. ch = this.code;
  594. }
  595. this.index++;
  596. if (ch === Jsep.OBRACK_CODE) {
  597. node = {
  598. type: Jsep.MEMBER_EXP,
  599. computed: true,
  600. object: node,
  601. property: this.gobbleExpression()
  602. };
  603. this.gobbleSpaces();
  604. ch = this.code;
  605. if (ch !== Jsep.CBRACK_CODE) {
  606. this.throwError('Unclosed [');
  607. }
  608. this.index++;
  609. }
  610. else if (ch === Jsep.OPAREN_CODE) {
  611. // A function call is being made; gobble all the arguments
  612. node = {
  613. type: Jsep.CALL_EXP,
  614. 'arguments': this.gobbleArguments(Jsep.CPAREN_CODE),
  615. callee: node
  616. };
  617. }
  618. else if (ch === Jsep.PERIOD_CODE || optional) {
  619. if (optional) {
  620. this.index--;
  621. }
  622. this.gobbleSpaces();
  623. node = {
  624. type: Jsep.MEMBER_EXP,
  625. computed: false,
  626. object: node,
  627. property: this.gobbleIdentifier(),
  628. };
  629. }
  630. if (optional) {
  631. node.optional = true;
  632. } // else leave undefined for compatibility with esprima
  633. this.gobbleSpaces();
  634. ch = this.code;
  635. }
  636. return node;
  637. }
  638. /**
  639. * Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
  640. * keep track of everything in the numeric literal and then calling `parseFloat` on that string
  641. * @returns {jsep.Literal}
  642. */
  643. gobbleNumericLiteral() {
  644. let number = '', ch, chCode;
  645. while (Jsep.isDecimalDigit(this.code)) {
  646. number += this.expr.charAt(this.index++);
  647. }
  648. if (this.code === Jsep.PERIOD_CODE) { // can start with a decimal marker
  649. number += this.expr.charAt(this.index++);
  650. while (Jsep.isDecimalDigit(this.code)) {
  651. number += this.expr.charAt(this.index++);
  652. }
  653. }
  654. ch = this.char;
  655. if (ch === 'e' || ch === 'E') { // exponent marker
  656. number += this.expr.charAt(this.index++);
  657. ch = this.char;
  658. if (ch === '+' || ch === '-') { // exponent sign
  659. number += this.expr.charAt(this.index++);
  660. }
  661. while (Jsep.isDecimalDigit(this.code)) { // exponent itself
  662. number += this.expr.charAt(this.index++);
  663. }
  664. if (!Jsep.isDecimalDigit(this.expr.charCodeAt(this.index - 1)) ) {
  665. this.throwError('Expected exponent (' + number + this.char + ')');
  666. }
  667. }
  668. chCode = this.code;
  669. // Check to make sure this isn't a variable name that start with a number (123abc)
  670. if (Jsep.isIdentifierStart(chCode)) {
  671. this.throwError('Variable names cannot start with a number (' +
  672. number + this.char + ')');
  673. }
  674. else if (chCode === Jsep.PERIOD_CODE || (number.length === 1 && number.charCodeAt(0) === Jsep.PERIOD_CODE)) {
  675. this.throwError('Unexpected period');
  676. }
  677. return {
  678. type: Jsep.LITERAL,
  679. value: parseFloat(number),
  680. raw: number
  681. };
  682. }
  683. /**
  684. * Parses a string literal, staring with single or double quotes with basic support for escape codes
  685. * e.g. `"hello world"`, `'this is\nJSEP'`
  686. * @returns {jsep.Literal}
  687. */
  688. gobbleStringLiteral() {
  689. let str = '';
  690. const startIndex = this.index;
  691. const quote = this.expr.charAt(this.index++);
  692. let closed = false;
  693. while (this.index < this.expr.length) {
  694. let ch = this.expr.charAt(this.index++);
  695. if (ch === quote) {
  696. closed = true;
  697. break;
  698. }
  699. else if (ch === '\\') {
  700. // Check for all of the common escape codes
  701. ch = this.expr.charAt(this.index++);
  702. switch (ch) {
  703. case 'n': str += '\n'; break;
  704. case 'r': str += '\r'; break;
  705. case 't': str += '\t'; break;
  706. case 'b': str += '\b'; break;
  707. case 'f': str += '\f'; break;
  708. case 'v': str += '\x0B'; break;
  709. default : str += ch;
  710. }
  711. }
  712. else {
  713. str += ch;
  714. }
  715. }
  716. if (!closed) {
  717. this.throwError('Unclosed quote after "' + str + '"');
  718. }
  719. return {
  720. type: Jsep.LITERAL,
  721. value: str,
  722. raw: this.expr.substring(startIndex, this.index),
  723. };
  724. }
  725. /**
  726. * Gobbles only identifiers
  727. * e.g.: `foo`, `_value`, `$x1`
  728. * Also, this function checks if that identifier is a literal:
  729. * (e.g. `true`, `false`, `null`) or `this`
  730. * @returns {jsep.Identifier}
  731. */
  732. gobbleIdentifier() {
  733. let ch = this.code, start = this.index;
  734. if (Jsep.isIdentifierStart(ch)) {
  735. this.index++;
  736. }
  737. else {
  738. this.throwError('Unexpected ' + this.char);
  739. }
  740. while (this.index < this.expr.length) {
  741. ch = this.code;
  742. if (Jsep.isIdentifierPart(ch)) {
  743. this.index++;
  744. }
  745. else {
  746. break;
  747. }
  748. }
  749. return {
  750. type: Jsep.IDENTIFIER,
  751. name: this.expr.slice(start, this.index),
  752. };
  753. }
  754. /**
  755. * Gobbles a list of arguments within the context of a function call
  756. * or array literal. This function also assumes that the opening character
  757. * `(` or `[` has already been gobbled, and gobbles expressions and commas
  758. * until the terminator character `)` or `]` is encountered.
  759. * e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
  760. * @param {number} termination
  761. * @returns {jsep.Expression[]}
  762. */
  763. gobbleArguments(termination) {
  764. const args = [];
  765. let closed = false;
  766. let separator_count = 0;
  767. while (this.index < this.expr.length) {
  768. this.gobbleSpaces();
  769. let ch_i = this.code;
  770. if (ch_i === termination) { // done parsing
  771. closed = true;
  772. this.index++;
  773. if (termination === Jsep.CPAREN_CODE && separator_count && separator_count >= args.length){
  774. this.throwError('Unexpected token ' + String.fromCharCode(termination));
  775. }
  776. break;
  777. }
  778. else if (ch_i === Jsep.COMMA_CODE) { // between expressions
  779. this.index++;
  780. separator_count++;
  781. if (separator_count !== args.length) { // missing argument
  782. if (termination === Jsep.CPAREN_CODE) {
  783. this.throwError('Unexpected token ,');
  784. }
  785. else if (termination === Jsep.CBRACK_CODE) {
  786. for (let arg = args.length; arg < separator_count; arg++) {
  787. args.push(null);
  788. }
  789. }
  790. }
  791. }
  792. else if (args.length !== separator_count && separator_count !== 0) {
  793. // NOTE: `&& separator_count !== 0` allows for either all commas, or all spaces as arguments
  794. this.throwError('Expected comma');
  795. }
  796. else {
  797. const node = this.gobbleExpression();
  798. if (!node || node.type === Jsep.COMPOUND) {
  799. this.throwError('Expected comma');
  800. }
  801. args.push(node);
  802. }
  803. }
  804. if (!closed) {
  805. this.throwError('Expected ' + String.fromCharCode(termination));
  806. }
  807. return args;
  808. }
  809. /**
  810. * Responsible for parsing a group of things within parentheses `()`
  811. * that have no identifier in front (so not a function call)
  812. * This function assumes that it needs to gobble the opening parenthesis
  813. * and then tries to gobble everything within that parenthesis, assuming
  814. * that the next thing it should see is the close parenthesis. If not,
  815. * then the expression probably doesn't have a `)`
  816. * @returns {boolean|jsep.Expression}
  817. */
  818. gobbleGroup() {
  819. this.index++;
  820. let nodes = this.gobbleExpressions(Jsep.CPAREN_CODE);
  821. if (this.code === Jsep.CPAREN_CODE) {
  822. this.index++;
  823. if (nodes.length === 1) {
  824. return nodes[0];
  825. }
  826. else if (!nodes.length) {
  827. return false;
  828. }
  829. else {
  830. return {
  831. type: Jsep.SEQUENCE_EXP,
  832. expressions: nodes,
  833. };
  834. }
  835. }
  836. else {
  837. this.throwError('Unclosed (');
  838. }
  839. }
  840. /**
  841. * Responsible for parsing Array literals `[1, 2, 3]`
  842. * This function assumes that it needs to gobble the opening bracket
  843. * and then tries to gobble the expressions as arguments.
  844. * @returns {jsep.ArrayExpression}
  845. */
  846. gobbleArray() {
  847. this.index++;
  848. return {
  849. type: Jsep.ARRAY_EXP,
  850. elements: this.gobbleArguments(Jsep.CBRACK_CODE)
  851. };
  852. }
  853. }
  854. // Static fields:
  855. const hooks = new Hooks();
  856. Object.assign(Jsep, {
  857. hooks,
  858. plugins: new Plugins(Jsep),
  859. // Node Types
  860. // ----------
  861. // This is the full set of types that any JSEP node can be.
  862. // Store them here to save space when minified
  863. COMPOUND: 'Compound',
  864. SEQUENCE_EXP: 'SequenceExpression',
  865. IDENTIFIER: 'Identifier',
  866. MEMBER_EXP: 'MemberExpression',
  867. LITERAL: 'Literal',
  868. THIS_EXP: 'ThisExpression',
  869. CALL_EXP: 'CallExpression',
  870. UNARY_EXP: 'UnaryExpression',
  871. BINARY_EXP: 'BinaryExpression',
  872. ARRAY_EXP: 'ArrayExpression',
  873. TAB_CODE: 9,
  874. LF_CODE: 10,
  875. CR_CODE: 13,
  876. SPACE_CODE: 32,
  877. PERIOD_CODE: 46, // '.'
  878. COMMA_CODE: 44, // ','
  879. SQUOTE_CODE: 39, // single quote
  880. DQUOTE_CODE: 34, // double quotes
  881. OPAREN_CODE: 40, // (
  882. CPAREN_CODE: 41, // )
  883. OBRACK_CODE: 91, // [
  884. CBRACK_CODE: 93, // ]
  885. QUMARK_CODE: 63, // ?
  886. SEMCOL_CODE: 59, // ;
  887. COLON_CODE: 58, // :
  888. // Operations
  889. // ----------
  890. // Use a quickly-accessible map to store all of the unary operators
  891. // Values are set to `1` (it really doesn't matter)
  892. unary_ops: {
  893. '-': 1,
  894. '!': 1,
  895. '~': 1,
  896. '+': 1
  897. },
  898. // Also use a map for the binary operations but set their values to their
  899. // binary precedence for quick reference (higher number = higher precedence)
  900. // see [Order of operations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)
  901. binary_ops: {
  902. '||': 1, '&&': 2, '|': 3, '^': 4, '&': 5,
  903. '==': 6, '!=': 6, '===': 6, '!==': 6,
  904. '<': 7, '>': 7, '<=': 7, '>=': 7,
  905. '<<': 8, '>>': 8, '>>>': 8,
  906. '+': 9, '-': 9,
  907. '*': 10, '/': 10, '%': 10
  908. },
  909. // sets specific binary_ops as right-associative
  910. right_associative: new Set(),
  911. // Additional valid identifier chars, apart from a-z, A-Z and 0-9 (except on the starting char)
  912. additional_identifier_chars: new Set(['$', '_']),
  913. // Literals
  914. // ----------
  915. // Store the values to return for the various literals we may encounter
  916. literals: {
  917. 'true': true,
  918. 'false': false,
  919. 'null': null
  920. },
  921. // Except for `this`, which is special. This could be changed to something like `'self'` as well
  922. this_str: 'this',
  923. });
  924. Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
  925. Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
  926. // Backward Compatibility:
  927. const jsep = expr => (new Jsep(expr)).parse();
  928. const staticMethods = Object.getOwnPropertyNames(Jsep);
  929. staticMethods
  930. .forEach((m) => {
  931. if (jsep[m] === undefined && m !== 'prototype') {
  932. jsep[m] = Jsep[m];
  933. }
  934. });
  935. jsep.Jsep = Jsep; // allows for const { Jsep } = require('jsep');
  936. const CONDITIONAL_EXP = 'ConditionalExpression';
  937. var ternary = {
  938. name: 'ternary',
  939. init(jsep) {
  940. // Ternary expression: test ? consequent : alternate
  941. jsep.hooks.add('after-expression', function gobbleTernary(env) {
  942. if (env.node && this.code === jsep.QUMARK_CODE) {
  943. this.index++;
  944. const test = env.node;
  945. const consequent = this.gobbleExpression();
  946. if (!consequent) {
  947. this.throwError('Expected expression');
  948. }
  949. this.gobbleSpaces();
  950. if (this.code === jsep.COLON_CODE) {
  951. this.index++;
  952. const alternate = this.gobbleExpression();
  953. if (!alternate) {
  954. this.throwError('Expected expression');
  955. }
  956. env.node = {
  957. type: CONDITIONAL_EXP,
  958. test,
  959. consequent,
  960. alternate,
  961. };
  962. // check for operators of higher priority than ternary (i.e. assignment)
  963. // jsep sets || at 1, and assignment at 0.9, and conditional should be between them
  964. if (test.operator && jsep.binary_ops[test.operator] <= 0.9) {
  965. let newTest = test;
  966. while (newTest.right.operator && jsep.binary_ops[newTest.right.operator] <= 0.9) {
  967. newTest = newTest.right;
  968. }
  969. env.node.test = newTest.right;
  970. newTest.right = env.node;
  971. env.node = test;
  972. }
  973. }
  974. else {
  975. this.throwError('Expected :');
  976. }
  977. }
  978. });
  979. },
  980. };
  981. // Add default plugins:
  982. jsep.plugins.register(ternary);
  983. export { Jsep, jsep as default };