@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.
- package/dist/index.cjs.js +4128 -891
- package/dist/index.d.ts +1227 -50
- package/dist/index.esm.js +4065 -836
- package/package.json +9 -15
- package/src/binding/binding.ts +108 -0
- package/src/binding/index.ts +188 -0
- package/src/binding/resolver.ts +157 -0
- package/src/binding/utils.ts +51 -0
- package/src/binding-grammar/ast.ts +113 -0
- package/src/binding-grammar/custom/index.ts +304 -0
- package/src/binding-grammar/ebnf/binding.ebnf +22 -0
- package/src/binding-grammar/ebnf/index.ts +186 -0
- package/src/binding-grammar/ebnf/types.ts +104 -0
- package/src/binding-grammar/index.ts +4 -0
- package/src/binding-grammar/parsimmon/index.ts +78 -0
- package/src/controllers/constants/index.ts +85 -0
- package/src/controllers/constants/utils.ts +37 -0
- package/src/{data.ts → controllers/data.ts} +6 -6
- package/src/controllers/flow/controller.ts +95 -0
- package/src/controllers/flow/flow.ts +205 -0
- package/src/controllers/flow/index.ts +2 -0
- package/src/controllers/index.ts +5 -0
- package/src/{validation → controllers/validation}/binding-tracker.ts +5 -5
- package/src/{validation → controllers/validation}/controller.ts +15 -14
- package/src/{validation → controllers/validation}/index.ts +0 -0
- package/src/{view → controllers/view}/asset-transform.ts +2 -3
- package/src/{view → controllers/view}/controller.ts +9 -8
- package/src/controllers/view/index.ts +4 -0
- package/src/{view → controllers/view}/store.ts +0 -0
- package/src/{view → controllers/view}/types.ts +2 -1
- package/src/data/dependency-tracker.ts +187 -0
- package/src/data/index.ts +4 -0
- package/src/data/local-model.ts +41 -0
- package/src/data/model.ts +216 -0
- package/src/data/noop-model.ts +18 -0
- package/src/expressions/evaluator-functions.ts +29 -0
- package/src/expressions/evaluator.ts +405 -0
- package/src/expressions/index.ts +3 -0
- package/src/expressions/parser.ts +889 -0
- package/src/expressions/types.ts +200 -0
- package/src/expressions/utils.ts +8 -0
- package/src/index.ts +9 -12
- package/src/logger/consoleLogger.ts +49 -0
- package/src/logger/index.ts +5 -0
- package/src/logger/noopLogger.ts +13 -0
- package/src/logger/proxyLogger.ts +25 -0
- package/src/logger/tapableLogger.ts +38 -0
- package/src/logger/types.ts +6 -0
- package/src/player.ts +21 -18
- package/src/plugins/flow-exp-plugin.ts +2 -3
- package/src/schema/index.ts +2 -0
- package/src/schema/schema.ts +220 -0
- package/src/schema/types.ts +60 -0
- package/src/string-resolver/index.ts +188 -0
- package/src/types.ts +11 -13
- package/src/utils/index.ts +1 -0
- package/src/utils/replaceParams.ts +17 -0
- package/src/validator/index.ts +3 -0
- package/src/validator/registry.ts +20 -0
- package/src/validator/types.ts +75 -0
- package/src/validator/validation-middleware.ts +114 -0
- package/src/view/builder/index.ts +81 -0
- package/src/view/index.ts +5 -4
- package/src/view/parser/index.ts +318 -0
- package/src/view/parser/types.ts +141 -0
- package/src/view/plugins/applicability.ts +78 -0
- package/src/view/plugins/index.ts +5 -0
- package/src/view/plugins/options.ts +4 -0
- package/src/view/plugins/plugin.ts +21 -0
- package/src/view/plugins/string-resolver.ts +149 -0
- package/src/view/plugins/switch.ts +120 -0
- package/src/view/plugins/template-plugin.ts +172 -0
- package/src/view/resolver/index.ts +397 -0
- package/src/view/resolver/types.ts +161 -0
- package/src/view/resolver/utils.ts +57 -0
- package/src/view/view.ts +149 -0
- 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
|
+
}
|