@player-ui/player 0.3.0-next.2 → 0.3.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/index.cjs.js +4128 -891
  2. package/dist/index.d.ts +1227 -50
  3. package/dist/index.esm.js +4065 -836
  4. package/package.json +9 -15
  5. package/src/binding/binding.ts +108 -0
  6. package/src/binding/index.ts +188 -0
  7. package/src/binding/resolver.ts +157 -0
  8. package/src/binding/utils.ts +51 -0
  9. package/src/binding-grammar/ast.ts +113 -0
  10. package/src/binding-grammar/custom/index.ts +304 -0
  11. package/src/binding-grammar/ebnf/binding.ebnf +22 -0
  12. package/src/binding-grammar/ebnf/index.ts +186 -0
  13. package/src/binding-grammar/ebnf/types.ts +104 -0
  14. package/src/binding-grammar/index.ts +4 -0
  15. package/src/binding-grammar/parsimmon/index.ts +78 -0
  16. package/src/controllers/constants/index.ts +85 -0
  17. package/src/controllers/constants/utils.ts +37 -0
  18. package/src/{data.ts → controllers/data.ts} +6 -6
  19. package/src/controllers/flow/controller.ts +95 -0
  20. package/src/controllers/flow/flow.ts +205 -0
  21. package/src/controllers/flow/index.ts +2 -0
  22. package/src/controllers/index.ts +5 -0
  23. package/src/{validation → controllers/validation}/binding-tracker.ts +5 -5
  24. package/src/{validation → controllers/validation}/controller.ts +15 -14
  25. package/src/{validation → controllers/validation}/index.ts +0 -0
  26. package/src/{view → controllers/view}/asset-transform.ts +2 -3
  27. package/src/{view → controllers/view}/controller.ts +9 -8
  28. package/src/controllers/view/index.ts +4 -0
  29. package/src/{view → controllers/view}/store.ts +0 -0
  30. package/src/{view → controllers/view}/types.ts +2 -1
  31. package/src/data/dependency-tracker.ts +187 -0
  32. package/src/data/index.ts +4 -0
  33. package/src/data/local-model.ts +41 -0
  34. package/src/data/model.ts +216 -0
  35. package/src/data/noop-model.ts +18 -0
  36. package/src/expressions/evaluator-functions.ts +29 -0
  37. package/src/expressions/evaluator.ts +405 -0
  38. package/src/expressions/index.ts +3 -0
  39. package/src/expressions/parser.ts +889 -0
  40. package/src/expressions/types.ts +200 -0
  41. package/src/expressions/utils.ts +8 -0
  42. package/src/index.ts +9 -12
  43. package/src/logger/consoleLogger.ts +49 -0
  44. package/src/logger/index.ts +5 -0
  45. package/src/logger/noopLogger.ts +13 -0
  46. package/src/logger/proxyLogger.ts +25 -0
  47. package/src/logger/tapableLogger.ts +38 -0
  48. package/src/logger/types.ts +6 -0
  49. package/src/player.ts +21 -18
  50. package/src/plugins/flow-exp-plugin.ts +2 -3
  51. package/src/schema/index.ts +2 -0
  52. package/src/schema/schema.ts +220 -0
  53. package/src/schema/types.ts +60 -0
  54. package/src/string-resolver/index.ts +188 -0
  55. package/src/types.ts +11 -13
  56. package/src/utils/index.ts +1 -0
  57. package/src/utils/replaceParams.ts +17 -0
  58. package/src/validator/index.ts +3 -0
  59. package/src/validator/registry.ts +20 -0
  60. package/src/validator/types.ts +75 -0
  61. package/src/validator/validation-middleware.ts +114 -0
  62. package/src/view/builder/index.ts +81 -0
  63. package/src/view/index.ts +5 -4
  64. package/src/view/parser/index.ts +318 -0
  65. package/src/view/parser/types.ts +141 -0
  66. package/src/view/plugins/applicability.ts +78 -0
  67. package/src/view/plugins/index.ts +5 -0
  68. package/src/view/plugins/options.ts +4 -0
  69. package/src/view/plugins/plugin.ts +21 -0
  70. package/src/view/plugins/string-resolver.ts +149 -0
  71. package/src/view/plugins/switch.ts +120 -0
  72. package/src/view/plugins/template-plugin.ts +172 -0
  73. package/src/view/resolver/index.ts +397 -0
  74. package/src/view/resolver/types.ts +161 -0
  75. package/src/view/resolver/utils.ts +57 -0
  76. package/src/view/view.ts +149 -0
  77. package/src/utils/desc.d.ts +0 -2
@@ -0,0 +1,889 @@
1
+ /* eslint @typescript-eslint/no-use-before-define: 0 */
2
+ /**
3
+ * An expression to AST parser based on JSEP: http://jsep.from.so/
4
+ */
5
+ import type { ExpressionNode, ExpressionNodeType } from './types';
6
+ import { ExpNodeOpaqueIdentifier } from './types';
7
+
8
+ const PERIOD_CODE = 46; // '.'
9
+ const COMMA_CODE = 44; // ','
10
+ const SQUOTE_CODE = 39; // Single quote
11
+ const DQUOTE_CODE = 34; // Double quotes
12
+ const OPAREN_CODE = 40; // (
13
+ const CPAREN_CODE = 41; // )
14
+ const OBRACK_CODE = 91; // [
15
+ const CBRACK_CODE = 93; // ]
16
+ const QUMARK_CODE = 63; // ?
17
+ const SEMCOL_CODE = 59; // ;
18
+ const COLON_CODE = 58; // :
19
+ const OCURL_CODE = 123; // {
20
+ const CCURL_CODE = 125; // }
21
+
22
+ // Operations
23
+ // ----------
24
+
25
+ // Set `t` to `true` to save space (when minified, not gzipped)
26
+ const t = true;
27
+
28
+ // Use a quickly-accessible map to store all of the unary operators
29
+ // Values are set to `true` (it really doesn't matter)
30
+ const unaryOps = { '-': t, '!': t, '~': t, '+': t };
31
+
32
+ // Also use a map for the binary operations but set their values to their
33
+ // binary precedence for quick reference:
34
+ // see [Operator precedence](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)
35
+ const binaryOps: Record<string, number> = {
36
+ '=': 3,
37
+ '+=': 3,
38
+ '-=': 3,
39
+ '&=': 3,
40
+ '|=': 3,
41
+ // Conditional: 4,
42
+ '||': 5,
43
+ '&&': 6,
44
+ '|': 7,
45
+ '^': 8,
46
+ '&': 9,
47
+ '==': 10,
48
+ '!=': 10,
49
+ '===': 10,
50
+ '!==': 10,
51
+ '<': 11,
52
+ '>': 11,
53
+ '<=': 11,
54
+ '>=': 11,
55
+ '<<': 12,
56
+ '>>': 12,
57
+ '>>>': 12,
58
+ '+': 13,
59
+ '-': 13,
60
+ '*': 14,
61
+ '/': 14,
62
+ '%': 14,
63
+ };
64
+
65
+ interface ErrorWithLocation extends Error {
66
+ /** The place in the string where the error occurs */
67
+ index: number;
68
+
69
+ /** a helpful description */
70
+ description: string;
71
+ }
72
+
73
+ /** Wrap the message and index in an error and throw it */
74
+ function throwError(message: string, index: number) {
75
+ const err = new Error(`${message} at character ${index}`);
76
+
77
+ (err as ErrorWithLocation).index = index;
78
+ (err as ErrorWithLocation).description = message;
79
+
80
+ throw err;
81
+ }
82
+
83
+ /** Get return the longest key length of any object */
84
+ function getMaxKeyLen(obj: object): number {
85
+ let maxLen = 0;
86
+
87
+ Object.keys(obj).forEach((key) => {
88
+ if (key.length > maxLen && Object.prototype.hasOwnProperty.call(obj, key)) {
89
+ maxLen = key.length;
90
+ }
91
+ });
92
+
93
+ return maxLen;
94
+ }
95
+
96
+ const maxUnopLen = getMaxKeyLen(unaryOps);
97
+ const maxBinopLen = getMaxKeyLen(binaryOps);
98
+
99
+ // Literals
100
+ // ----------
101
+ // Store the values to return for the various literals we may encounter
102
+ const literals = {
103
+ true: true,
104
+ false: false,
105
+ null: null,
106
+ undefined,
107
+ } as const;
108
+
109
+ // Except for `this`, which is special. This could be changed to something like `'self'` as well
110
+ const thisStr = 'this';
111
+
112
+ /** Returns the precedence of a binary operator or `0` if it isn't a binary operator */
113
+ function binaryPrecedence(opVal: string): number {
114
+ return binaryOps[opVal] || 0;
115
+ }
116
+
117
+ /**
118
+ * Utility function (gets called from multiple places)
119
+ * Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions
120
+ */
121
+ function createBinaryExpression(
122
+ operator: string | boolean,
123
+ left: string,
124
+ right: string
125
+ ) {
126
+ let type: ExpressionNodeType;
127
+
128
+ if (operator === '||' || operator === '&&') {
129
+ type = 'LogicalExpression';
130
+ } else if (operator === '=') {
131
+ type = 'Assignment';
132
+ } else if (
133
+ operator === '+=' ||
134
+ operator === '-=' ||
135
+ operator === '&=' ||
136
+ operator === '|='
137
+ ) {
138
+ type = 'Modification';
139
+ } else {
140
+ type = 'BinaryExpression';
141
+ }
142
+
143
+ return {
144
+ __id: ExpNodeOpaqueIdentifier,
145
+ type,
146
+ operator,
147
+ left,
148
+ right,
149
+ };
150
+ }
151
+
152
+ /** `ch` is a character code in the next three functions */
153
+ function isDecimalDigit(ch: number) {
154
+ return ch >= 48 && ch <= 57; // 0...9
155
+ }
156
+
157
+ /** Check if the char is the character code for the start of an identifier */
158
+ function isIdentifierStart(ch: number) {
159
+ return (
160
+ ch === 36 ||
161
+ ch === 95 || // `$` and `_`
162
+ (ch >= 65 && ch <= 90) || // A...Z
163
+ (ch >= 97 && ch <= 122)
164
+ ); // A...z
165
+ }
166
+
167
+ /** Check if the char code is still a valid identifier portion */
168
+ function isIdentifierPart(ch: number) {
169
+ return (
170
+ ch === 36 ||
171
+ ch === 95 || // `$` and `_`
172
+ (ch >= 65 && ch <= 90) || // A...Z
173
+ (ch >= 97 && ch <= 122) || // A...z
174
+ (ch >= 48 && ch <= 57)
175
+ ); // 0...9
176
+ }
177
+
178
+ /** Check if the 2 chars are the start of a model reference */
179
+ function isModelRefStart(ch0: number, ch1: number) {
180
+ return ch0 === OCURL_CODE && ch1 === OCURL_CODE; // '{{'
181
+ }
182
+
183
+ /** Parse out an expression from the string */
184
+ export default function parseExpression(expr: string): ExpressionNode {
185
+ // `index` stores the character number we are currently at while `length` is a constant
186
+ // All of the gobbles below will modify `index` as we move along
187
+ const charAtFunc = expr.charAt;
188
+ const charCodeAtFunc = expr.charCodeAt;
189
+ const { length } = expr;
190
+
191
+ let index = 0;
192
+
193
+ /** Grab the char at the index from the expression */
194
+ function exprI(i: number) {
195
+ return charAtFunc.call(expr, i);
196
+ }
197
+
198
+ /** Grab the unicode char at the index in the expression */
199
+ function exprICode(i: number) {
200
+ return charCodeAtFunc.call(expr, i);
201
+ }
202
+
203
+ /**
204
+ * Gobble an object and store the object in an attributes array
205
+ */
206
+ function gobbleObjects() {
207
+ const attributes: Array<{
208
+ /** The property name of the object */
209
+ key: any;
210
+
211
+ /** the associated value */
212
+ value: any;
213
+ }> = [];
214
+ let closed = false;
215
+
216
+ let shouldDefineKey = true;
217
+ let key;
218
+ let value;
219
+ let chCode;
220
+ // get rid of OCURL_CODE
221
+ ++index;
222
+
223
+ while (index < length) {
224
+ gobbleSpaces();
225
+ chCode = exprICode(index);
226
+
227
+ // check for end
228
+ if (chCode === CCURL_CODE) {
229
+ // if we are at the end but a key was defined
230
+ if (key) {
231
+ throwError('A key was defined but a value was not', index);
232
+ }
233
+
234
+ index++;
235
+ closed = true;
236
+ break;
237
+ } else if (shouldDefineKey) {
238
+ // check for key
239
+ if (chCode !== SQUOTE_CODE && chCode !== DQUOTE_CODE) {
240
+ throwError('An object must start wtih a key', index);
241
+ }
242
+
243
+ // get key
244
+ key = gobbleStringLiteral();
245
+ // remove spaces
246
+ gobbleSpaces();
247
+
248
+ // remove colon
249
+ if (exprICode(index) === COLON_CODE) {
250
+ index++;
251
+ shouldDefineKey = false;
252
+ } else {
253
+ throwError('A colon must follow an object key', index);
254
+ }
255
+ } else {
256
+ value = gobbleExpression();
257
+
258
+ attributes.push({ key, value });
259
+ gobbleSpaces();
260
+ chCode = exprICode(index);
261
+
262
+ if (chCode === COMMA_CODE) {
263
+ index++;
264
+ } else if (chCode !== CCURL_CODE) {
265
+ throwError('Please add a comma to add another key', index);
266
+ }
267
+
268
+ shouldDefineKey = true;
269
+ key = undefined;
270
+ value = undefined;
271
+ }
272
+
273
+ chCode = exprICode(index);
274
+ }
275
+
276
+ // throw error if object is not closed
277
+ if (!closed) {
278
+ throwError(`Unclosed brace in object`, index);
279
+ }
280
+
281
+ return {
282
+ __id: ExpNodeOpaqueIdentifier,
283
+ type: 'Object',
284
+ attributes,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Push `index` up to the next non-space character
290
+ */
291
+ function gobbleSpaces() {
292
+ let ch = exprICode(index);
293
+
294
+ // Space or tab
295
+ while (ch === 32 || ch === 9) {
296
+ ch = exprICode(++index);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * The main parsing function. Much of this code is dedicated to ternary expressions
302
+ */
303
+ function gobbleExpression(): ExpressionNode {
304
+ const test = gobbleBinaryExpression();
305
+ gobbleSpaces();
306
+
307
+ if (index < length && exprICode(index) === QUMARK_CODE) {
308
+ // Ternary expression: test ? consequent : alternate
309
+ index++;
310
+ const consequent = gobbleExpression();
311
+
312
+ if (!consequent) {
313
+ throwError('Expected expression', index);
314
+ }
315
+
316
+ gobbleSpaces();
317
+
318
+ if (exprICode(index) === COLON_CODE) {
319
+ index++;
320
+ const alternate = gobbleExpression();
321
+
322
+ if (!alternate) {
323
+ throwError('Expected expression', index);
324
+ }
325
+
326
+ return {
327
+ __id: ExpNodeOpaqueIdentifier,
328
+ type: 'ConditionalExpression',
329
+ test,
330
+ consequent,
331
+ alternate,
332
+ };
333
+ }
334
+
335
+ throwError('Expected :', index);
336
+ }
337
+
338
+ return test;
339
+ }
340
+
341
+ /**
342
+ * Search for the operation portion of the string (e.g. `+`, `===`)
343
+ * Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
344
+ * and move down from 3 to 2 to 1 character until a matching binary operation is found
345
+ * then, return that binary operation
346
+ */
347
+ function gobbleBinaryOp() {
348
+ gobbleSpaces();
349
+
350
+ let toCheck = expr.substr(index, maxBinopLen);
351
+ let tcLen = toCheck.length;
352
+
353
+ while (tcLen > 0) {
354
+ if (Object.prototype.hasOwnProperty.call(binaryOps, toCheck)) {
355
+ index += tcLen;
356
+
357
+ return toCheck;
358
+ }
359
+
360
+ toCheck = toCheck.substr(0, --tcLen);
361
+ }
362
+
363
+ return false;
364
+ }
365
+
366
+ /**
367
+ * This function is responsible for gobbling an individual expression,
368
+ * e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
369
+ */
370
+ function gobbleBinaryExpression() {
371
+ let node;
372
+ let prec;
373
+ let i;
374
+
375
+ // First, try to get the leftmost thing
376
+ // Then, check to see if there's a binary operator operating on that leftmost thing
377
+ let left = gobbleToken();
378
+ let biop = gobbleBinaryOp();
379
+
380
+ // If there wasn't a binary operator, just return the leftmost node
381
+ if (!biop) {
382
+ return left;
383
+ }
384
+
385
+ // Otherwise, we need to start a stack to properly place the binary operations in their
386
+ // precedence structure
387
+ let biopInfo = { value: biop, prec: binaryPrecedence(biop) };
388
+ let right = gobbleToken();
389
+
390
+ if (!right) {
391
+ throwError(`Expected expression after ${biop}`, index);
392
+ }
393
+
394
+ const stack = [left, biopInfo, right];
395
+
396
+ // Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
397
+ biop = gobbleBinaryOp();
398
+
399
+ while (biop) {
400
+ prec = binaryPrecedence(biop);
401
+
402
+ if (prec === 0) {
403
+ break;
404
+ }
405
+
406
+ biopInfo = { value: biop, prec };
407
+
408
+ // Reduce: make a binary expression from the three topmost entries.
409
+ while (stack.length > 2 && prec <= stack[stack.length - 2].prec) {
410
+ right = stack.pop();
411
+ biop = stack.pop().value;
412
+ left = stack.pop();
413
+ node = createBinaryExpression(biop, left, right);
414
+ stack.push(node);
415
+ }
416
+
417
+ node = gobbleToken();
418
+
419
+ if (!node) {
420
+ throwError(`Expected expression after ${biop}`, index);
421
+ }
422
+
423
+ stack.push(biopInfo, node);
424
+ biop = gobbleBinaryOp();
425
+ }
426
+
427
+ i = stack.length - 1;
428
+ node = stack[i];
429
+
430
+ while (i > 1) {
431
+ node = createBinaryExpression(stack[i - 1].value, stack[i - 2], node);
432
+ i -= 2;
433
+ }
434
+
435
+ return node;
436
+ }
437
+
438
+ /**
439
+ * An individual part of a binary expression:
440
+ * e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
441
+ */
442
+ function gobbleToken(): any {
443
+ gobbleSpaces();
444
+ const ch = exprICode(index);
445
+
446
+ if (isDecimalDigit(ch) || ch === PERIOD_CODE) {
447
+ // Char code 46 is a dot `.` which can start off a numeric literal
448
+ return gobbleNumericLiteral();
449
+ }
450
+
451
+ if (ch === SQUOTE_CODE || ch === DQUOTE_CODE) {
452
+ // Single or double quotes
453
+ return gobbleStringLiteral();
454
+ }
455
+
456
+ if (isIdentifierStart(ch) || ch === OPAREN_CODE) {
457
+ // Open parenthesis
458
+ // `foo`, `bar.baz`
459
+ return gobbleVariable();
460
+ }
461
+
462
+ if (ch === OBRACK_CODE) {
463
+ return gobbleArray();
464
+ }
465
+
466
+ if (isModelRefStart(ch, exprICode(index + 1))) {
467
+ return gobbleModelRef();
468
+ }
469
+
470
+ // not a double bracket: {{}} but if its a single {}
471
+ if (ch === OCURL_CODE) {
472
+ return gobbleObjects();
473
+ }
474
+
475
+ let toCheck = expr.substr(index, maxUnopLen);
476
+ let tcLen = toCheck.length;
477
+
478
+ while (tcLen > 0) {
479
+ if (Object.prototype.hasOwnProperty.call(unaryOps, toCheck)) {
480
+ index += tcLen;
481
+
482
+ return {
483
+ __id: ExpNodeOpaqueIdentifier,
484
+ type: 'UnaryExpression',
485
+ operator: toCheck,
486
+ argument: gobbleToken(),
487
+ prefix: true,
488
+ };
489
+ }
490
+
491
+ toCheck = toCheck.substr(0, --tcLen);
492
+ }
493
+
494
+ return false;
495
+ }
496
+
497
+ /**
498
+ * Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
499
+ * keep track of everything in the numeric literal and then calling `parseFloat` on that string
500
+ */
501
+ function gobbleNumericLiteral() {
502
+ let num = '';
503
+
504
+ while (isDecimalDigit(exprICode(index))) {
505
+ num += exprI(index++);
506
+ }
507
+
508
+ if (exprICode(index) === PERIOD_CODE) {
509
+ // Can start with a decimal marker
510
+ num += exprI(index++);
511
+
512
+ while (isDecimalDigit(exprICode(index))) {
513
+ num += exprI(index++);
514
+ }
515
+ }
516
+
517
+ let ch = exprI(index);
518
+
519
+ if (ch === 'e' || ch === 'E') {
520
+ // Exponent marker
521
+ num += exprI(index++);
522
+ ch = exprI(index);
523
+
524
+ if (ch === '+' || ch === '-') {
525
+ // Exponent sign
526
+ num += exprI(index++);
527
+ }
528
+
529
+ while (isDecimalDigit(exprICode(index))) {
530
+ // Exponent itself
531
+ num += exprI(index++);
532
+ }
533
+
534
+ if (!isDecimalDigit(exprICode(index - 1))) {
535
+ throwError(`Expected exponent (${num}${exprI(index)})`, index);
536
+ }
537
+ }
538
+
539
+ const chCode = exprICode(index);
540
+
541
+ // Check to make sure this isn't a variable name that start with a number (123abc)
542
+ if (isIdentifierStart(chCode)) {
543
+ throwError(
544
+ `Variable names cannot start with a number (${num}${exprI(index)})`,
545
+ index
546
+ );
547
+ } else if (chCode === PERIOD_CODE) {
548
+ throwError('Unexpected period', index);
549
+ }
550
+
551
+ return {
552
+ __id: ExpNodeOpaqueIdentifier,
553
+ type: 'Literal',
554
+ value: parseFloat(num),
555
+ raw: num,
556
+ };
557
+ }
558
+
559
+ /**
560
+ * Parses a string literal, staring with single or double quotes with basic support for escape codes
561
+ * e.g. `"hello world"`, `'this is\nJSEP'`
562
+ */
563
+ function gobbleStringLiteral() {
564
+ const quote = exprI(index++);
565
+ let str = '';
566
+ let closed = false;
567
+
568
+ while (index < length) {
569
+ let ch = exprI(index++);
570
+
571
+ if (ch === quote) {
572
+ closed = true;
573
+ break;
574
+ }
575
+
576
+ if (ch !== '\\') {
577
+ str += ch;
578
+ continue;
579
+ }
580
+
581
+ // Check for all of the common escape codes
582
+ ch = exprI(index++);
583
+
584
+ switch (ch) {
585
+ case 'n':
586
+ str += '\n';
587
+ break;
588
+ case 'r':
589
+ str += '\r';
590
+ break;
591
+ case 't':
592
+ str += '\t';
593
+ break;
594
+ case 'b':
595
+ str += '\b';
596
+ break;
597
+ case 'f':
598
+ str += '\f';
599
+ break;
600
+ case 'v':
601
+ str += '\u000B';
602
+ break;
603
+ default:
604
+ }
605
+ }
606
+
607
+ if (!closed) {
608
+ throwError(`Unclosed quote after "${str}"`, index);
609
+ }
610
+
611
+ return {
612
+ __id: ExpNodeOpaqueIdentifier,
613
+ type: 'Literal',
614
+ value: str,
615
+ raw: `${quote}${str}${quote}`,
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Model refs are bindings wrapped in 2 sets of double curlys
621
+ * e.g. {{foo.bar.ref}}
622
+ */
623
+ function gobbleModelRef() {
624
+ let str = '';
625
+ let closed = false;
626
+ let openBraceCount = 1;
627
+
628
+ index += 2; // Skip the {{
629
+
630
+ while (index < length) {
631
+ const ch = exprI(index++);
632
+
633
+ if (ch === '}' && exprICode(index) === CCURL_CODE) {
634
+ index++;
635
+ openBraceCount--;
636
+
637
+ if (openBraceCount === 0) {
638
+ closed = true;
639
+ break;
640
+ }
641
+
642
+ str += '}}';
643
+ } else if (ch === '{' && exprICode(index) === OCURL_CODE) {
644
+ openBraceCount++;
645
+ str += '{{';
646
+ index++;
647
+ } else {
648
+ str += ch;
649
+ }
650
+ }
651
+
652
+ if (!closed) {
653
+ throwError(`Unclosed brace after "${str}"`, index);
654
+ }
655
+
656
+ return {
657
+ __id: ExpNodeOpaqueIdentifier,
658
+ type: 'ModelRef',
659
+ ref: str,
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Gobbles only identifiers
665
+ * e.g.: `foo`, `_value`, `$x1`
666
+ * Also, this function checks if that identifier is a literal:
667
+ * (e.g. `true`, `false`, `null`) or `this`
668
+ */
669
+ function gobbleIdentifier() {
670
+ const start = index;
671
+ let ch = exprICode(start);
672
+
673
+ if (isIdentifierStart(ch)) {
674
+ index++;
675
+ } else {
676
+ throwError(`Unexpected ${exprI(index)}`, index);
677
+ }
678
+
679
+ while (index < length) {
680
+ ch = exprICode(index);
681
+
682
+ if (isIdentifierPart(ch)) {
683
+ index++;
684
+ } else {
685
+ break;
686
+ }
687
+ }
688
+
689
+ const identifier = expr.slice(start, index);
690
+
691
+ if (Object.prototype.hasOwnProperty.call(literals, identifier)) {
692
+ return {
693
+ __id: ExpNodeOpaqueIdentifier,
694
+ type: 'Literal',
695
+ value: (literals as any)[identifier],
696
+ raw: identifier,
697
+ };
698
+ }
699
+
700
+ if (identifier === thisStr) {
701
+ return {
702
+ __id: ExpNodeOpaqueIdentifier,
703
+ type: 'ThisExpression',
704
+ };
705
+ }
706
+
707
+ return {
708
+ __id: ExpNodeOpaqueIdentifier,
709
+ type: 'Identifier',
710
+ name: identifier,
711
+ };
712
+ }
713
+
714
+ /**
715
+ * Gobbles a list of arguments within the context of a function call
716
+ * or array literal. This function also assumes that the opening character
717
+ * `(` or `[` has already been gobbled, and gobbles expressions and commas
718
+ * until the terminator character `)` or `]` is encountered.
719
+ * e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
720
+ */
721
+ function gobbleArguments(termination: number) {
722
+ const args = [];
723
+ let charIndex;
724
+ let node;
725
+
726
+ while (index < length) {
727
+ gobbleSpaces();
728
+ charIndex = exprICode(index);
729
+
730
+ if (charIndex === termination) {
731
+ // Done parsing
732
+ index++;
733
+ break;
734
+ }
735
+
736
+ if (charIndex === COMMA_CODE) {
737
+ // Between expressions
738
+ index++;
739
+ continue;
740
+ }
741
+
742
+ node = gobbleExpression();
743
+
744
+ if (!node || node.type === 'Compound') {
745
+ throwError('Expected comma', index);
746
+ }
747
+
748
+ args.push(node);
749
+ }
750
+
751
+ return args;
752
+ }
753
+
754
+ /**
755
+ * Gobble a non-literal variable name. This variable name may include properties
756
+ * e.g. `foo`, `bar.baz`, `foo['bar'].baz`
757
+ * It also gobbles function calls:
758
+ * e.g. `Math.acos(obj.angle)`
759
+ */
760
+ function gobbleVariable(): ExpressionNode {
761
+ let charIndex = exprICode(index);
762
+ let node: any =
763
+ charIndex === OPAREN_CODE ? gobbleGroup() : gobbleIdentifier();
764
+
765
+ gobbleSpaces();
766
+ charIndex = exprICode(index);
767
+
768
+ while (
769
+ charIndex === PERIOD_CODE ||
770
+ charIndex === OBRACK_CODE ||
771
+ charIndex === OPAREN_CODE
772
+ ) {
773
+ index++;
774
+
775
+ if (charIndex === PERIOD_CODE) {
776
+ gobbleSpaces();
777
+
778
+ node = {
779
+ __id: ExpNodeOpaqueIdentifier,
780
+ type: 'MemberExpression',
781
+ computed: false,
782
+ object: node,
783
+ property: gobbleIdentifier(),
784
+ };
785
+ } else if (charIndex === OBRACK_CODE) {
786
+ node = {
787
+ __id: ExpNodeOpaqueIdentifier,
788
+ type: 'MemberExpression',
789
+ computed: true,
790
+ object: node,
791
+ property: gobbleExpression(),
792
+ };
793
+
794
+ gobbleSpaces();
795
+ charIndex = exprICode(index);
796
+
797
+ if (charIndex !== CBRACK_CODE) {
798
+ throwError('Unclosed [', index);
799
+ }
800
+
801
+ index++;
802
+ } else if (charIndex === OPAREN_CODE) {
803
+ // A function call is being made; gobble all the arguments
804
+ node = {
805
+ __id: ExpNodeOpaqueIdentifier,
806
+ type: 'CallExpression',
807
+ args: gobbleArguments(CPAREN_CODE),
808
+ callTarget: node,
809
+ };
810
+ }
811
+
812
+ gobbleSpaces();
813
+ charIndex = exprICode(index);
814
+ }
815
+
816
+ return node;
817
+ }
818
+
819
+ /**
820
+ * Responsible for parsing a group within parentheses `()`
821
+ * This function assumes that it needs to gobble the opening parenthesis
822
+ * and then tries to gobble everything within that parenthesis, assuming
823
+ * that the next thing it should see is the close parenthesis. If not,
824
+ * then the expression probably doesn't have a `)`
825
+ */
826
+ function gobbleGroup() {
827
+ index++;
828
+ const node = gobbleExpression();
829
+ gobbleSpaces();
830
+
831
+ if (exprICode(index) === CPAREN_CODE) {
832
+ index++;
833
+
834
+ return node;
835
+ }
836
+
837
+ throwError('Unclosed (', index);
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
+ */
845
+ function gobbleArray() {
846
+ index++;
847
+
848
+ return {
849
+ __id: ExpNodeOpaqueIdentifier,
850
+ type: 'ArrayExpression',
851
+ elements: gobbleArguments(CBRACK_CODE),
852
+ };
853
+ }
854
+
855
+ const nodes = [];
856
+
857
+ while (index < length) {
858
+ const chIndex = exprICode(index);
859
+
860
+ // Expressions can be separated by semicolons, commas, or just inferred without any
861
+ // separators
862
+ if (chIndex === SEMCOL_CODE || chIndex === COMMA_CODE) {
863
+ index++; // ignore separators
864
+ continue;
865
+ }
866
+
867
+ const node = gobbleExpression();
868
+
869
+ // Try to gobble each expression individually
870
+ if (node) {
871
+ nodes.push(node);
872
+ // If we weren't able to find a binary expression and are out of room, then
873
+ // the expression passed in probably has too much
874
+ } else if (index < length) {
875
+ throwError(`Unexpected "${exprI(index)}"`, index);
876
+ }
877
+ }
878
+
879
+ // If there's only one expression just try returning the expression
880
+ if (nodes.length === 1) {
881
+ return nodes[0];
882
+ }
883
+
884
+ return {
885
+ __id: ExpNodeOpaqueIdentifier,
886
+ type: 'Compound',
887
+ body: nodes,
888
+ };
889
+ }