@player-ui/player 0.3.1-next.1 → 0.4.0-next.1
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 +197 -39
- package/dist/index.d.ts +38 -13
- package/dist/index.esm.js +197 -40
- package/package.json +5 -4
- package/src/binding/resolver.ts +1 -3
- package/src/controllers/data.ts +4 -2
- package/src/controllers/validation/controller.ts +26 -13
- package/src/data/model.ts +5 -0
- package/src/expressions/evaluator.ts +1 -1
- package/src/expressions/parser.ts +64 -18
- package/src/expressions/types.ts +27 -12
- package/src/expressions/utils.ts +124 -1
- package/src/player.ts +28 -13
- package/src/string-resolver/index.ts +1 -0
- package/src/validator/validation-middleware.ts +21 -3
- package/src/view/plugins/string-resolver.ts +27 -5
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ValidationProvider,
|
|
13
13
|
ValidationResponse,
|
|
14
14
|
WarningValidationResponse,
|
|
15
|
+
StrongOrWeakBinding,
|
|
15
16
|
} from '../../validator';
|
|
16
17
|
import { ValidationMiddleware, ValidatorRegistry } from '../../validator';
|
|
17
18
|
import type { Logger } from '../../logger';
|
|
@@ -326,24 +327,33 @@ export class ValidationController implements BindingTracker {
|
|
|
326
327
|
const strongValidation = this.getValidationForBinding(binding);
|
|
327
328
|
|
|
328
329
|
// return validation issues directly on bindings first
|
|
329
|
-
if (strongValidation?.get()
|
|
330
|
+
if (strongValidation?.get()?.severity === 'error') {
|
|
331
|
+
return strongValidation.get();
|
|
332
|
+
}
|
|
330
333
|
|
|
331
334
|
// if none, check to see any validations this binding may be a weak ref of and return
|
|
332
|
-
const newInvalidBindings: Set<
|
|
333
|
-
|
|
335
|
+
const newInvalidBindings: Set<StrongOrWeakBinding> = new Set();
|
|
336
|
+
this.validations.forEach((weakValidation, strongBinding) => {
|
|
334
337
|
if (
|
|
335
338
|
caresAboutDataChanges(
|
|
336
339
|
new Set([binding]),
|
|
337
340
|
weakValidation.weakBindings
|
|
338
341
|
) &&
|
|
339
|
-
weakValidation?.get()
|
|
342
|
+
weakValidation?.get()?.severity === 'error'
|
|
340
343
|
) {
|
|
341
|
-
weakValidation?.weakBindings.forEach(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
weakValidation?.weakBindings.forEach((weakBinding) => {
|
|
345
|
+
weakBinding === strongBinding
|
|
346
|
+
? newInvalidBindings.add({
|
|
347
|
+
binding: weakBinding,
|
|
348
|
+
isStrong: true,
|
|
349
|
+
})
|
|
350
|
+
: newInvalidBindings.add({
|
|
351
|
+
binding: weakBinding,
|
|
352
|
+
isStrong: false,
|
|
353
|
+
});
|
|
354
|
+
});
|
|
345
355
|
}
|
|
346
|
-
}
|
|
356
|
+
});
|
|
347
357
|
|
|
348
358
|
if (newInvalidBindings.size > 0) {
|
|
349
359
|
return newInvalidBindings;
|
|
@@ -376,7 +386,10 @@ export class ValidationController implements BindingTracker {
|
|
|
376
386
|
});
|
|
377
387
|
|
|
378
388
|
if (originalValue !== withoutDefault) {
|
|
379
|
-
|
|
389
|
+
// Don't trigger updates when setting the default value
|
|
390
|
+
this.options.model.set([[binding, originalValue]], {
|
|
391
|
+
silent: true,
|
|
392
|
+
});
|
|
380
393
|
}
|
|
381
394
|
|
|
382
395
|
this.updateValidationsForBinding(
|
|
@@ -452,7 +465,7 @@ export class ValidationController implements BindingTracker {
|
|
|
452
465
|
const response = this.validationRunner(
|
|
453
466
|
validationObj,
|
|
454
467
|
context,
|
|
455
|
-
|
|
468
|
+
vBinding
|
|
456
469
|
);
|
|
457
470
|
return response ? { message: response.message } : undefined;
|
|
458
471
|
});
|
|
@@ -582,7 +595,7 @@ export class ValidationController implements BindingTracker {
|
|
|
582
595
|
|
|
583
596
|
const validations = new Map<BindingInstance, ValidationResponse>();
|
|
584
597
|
|
|
585
|
-
|
|
598
|
+
this.getBindings().forEach((b) => {
|
|
586
599
|
const invalid = this.getValidationForBinding(b)?.get();
|
|
587
600
|
|
|
588
601
|
if (invalid) {
|
|
@@ -594,7 +607,7 @@ export class ValidationController implements BindingTracker {
|
|
|
594
607
|
|
|
595
608
|
validations.set(b, invalid);
|
|
596
609
|
}
|
|
597
|
-
}
|
|
610
|
+
});
|
|
598
611
|
|
|
599
612
|
return {
|
|
600
613
|
canTransition: validations.size === 0,
|
package/src/data/model.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface DataModelOptions {
|
|
|
41
41
|
*/
|
|
42
42
|
ignoreDefaultValue?: boolean;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A flag to indicate that this update should happen silently
|
|
46
|
+
*/
|
|
47
|
+
silent?: boolean;
|
|
48
|
+
|
|
44
49
|
/** Other context associated with this request */
|
|
45
50
|
context?: {
|
|
46
51
|
/** The data model to use when getting other data from the context of this request */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SyncWaterfallHook, SyncBailHook } from 'tapable-ts';
|
|
2
2
|
import parse from './parser';
|
|
3
3
|
import * as DEFAULT_EXPRESSION_HANDLERS from './evaluator-functions';
|
|
4
|
+
import { isExpressionNode } from './types';
|
|
4
5
|
import type {
|
|
5
6
|
ExpressionNode,
|
|
6
7
|
BinaryOperator,
|
|
@@ -9,7 +10,6 @@ import type {
|
|
|
9
10
|
ExpressionContext,
|
|
10
11
|
ExpressionHandler,
|
|
11
12
|
} from './types';
|
|
12
|
-
import { isExpressionNode } from '.';
|
|
13
13
|
|
|
14
14
|
/** a && b -- but handles short cutting if the first value is false */
|
|
15
15
|
const andandOperator: BinaryOperator = (ctx, a, b) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* An expression to AST parser based on JSEP: http://jsep.from.so/
|
|
4
4
|
*/
|
|
5
|
-
import type { ExpressionNode, ExpressionNodeType } from './types';
|
|
5
|
+
import type { ExpressionNode, ExpressionNodeType, NodeLocation } from './types';
|
|
6
6
|
import { ExpNodeOpaqueIdentifier } from './types';
|
|
7
7
|
|
|
8
8
|
const PERIOD_CODE = 46; // '.'
|
|
@@ -80,6 +80,18 @@ function throwError(message: string, index: number) {
|
|
|
80
80
|
throw err;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Create a new location marker that spans both nodes */
|
|
84
|
+
function createSpanningLocation(start?: NodeLocation, end?: NodeLocation) {
|
|
85
|
+
if (!start || !end) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
start: start.start,
|
|
91
|
+
end: end.end,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
/** Get return the longest key length of any object */
|
|
84
96
|
function getMaxKeyLen(obj: object): number {
|
|
85
97
|
let maxLen = 0;
|
|
@@ -121,7 +133,8 @@ function binaryPrecedence(opVal: string): number {
|
|
|
121
133
|
function createBinaryExpression(
|
|
122
134
|
operator: string | boolean,
|
|
123
135
|
left: string,
|
|
124
|
-
right: string
|
|
136
|
+
right: string,
|
|
137
|
+
location?: NodeLocation
|
|
125
138
|
) {
|
|
126
139
|
let type: ExpressionNodeType;
|
|
127
140
|
|
|
@@ -146,6 +159,7 @@ function createBinaryExpression(
|
|
|
146
159
|
operator,
|
|
147
160
|
left,
|
|
148
161
|
right,
|
|
162
|
+
location,
|
|
149
163
|
};
|
|
150
164
|
}
|
|
151
165
|
|
|
@@ -190,6 +204,18 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
190
204
|
|
|
191
205
|
let index = 0;
|
|
192
206
|
|
|
207
|
+
/** Create a location object */
|
|
208
|
+
const getLocation = (startChar: number) => {
|
|
209
|
+
return {
|
|
210
|
+
start: {
|
|
211
|
+
character: startChar,
|
|
212
|
+
},
|
|
213
|
+
end: {
|
|
214
|
+
character: index,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
193
219
|
/** Grab the char at the index from the expression */
|
|
194
220
|
function exprI(i: number) {
|
|
195
221
|
return charAtFunc.call(expr, i);
|
|
@@ -217,13 +243,14 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
217
243
|
let key;
|
|
218
244
|
let value;
|
|
219
245
|
let chCode;
|
|
246
|
+
const startCharIndex = index;
|
|
247
|
+
|
|
220
248
|
// get rid of OCURL_CODE
|
|
221
249
|
++index;
|
|
222
250
|
|
|
223
251
|
while (index < length) {
|
|
224
252
|
gobbleSpaces();
|
|
225
253
|
chCode = exprICode(index);
|
|
226
|
-
|
|
227
254
|
// check for end
|
|
228
255
|
if (chCode === CCURL_CODE) {
|
|
229
256
|
// if we are at the end but a key was defined
|
|
@@ -244,7 +271,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
244
271
|
key = gobbleStringLiteral();
|
|
245
272
|
// remove spaces
|
|
246
273
|
gobbleSpaces();
|
|
247
|
-
|
|
248
274
|
// remove colon
|
|
249
275
|
if (exprICode(index) === COLON_CODE) {
|
|
250
276
|
index++;
|
|
@@ -258,7 +284,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
258
284
|
attributes.push({ key, value });
|
|
259
285
|
gobbleSpaces();
|
|
260
286
|
chCode = exprICode(index);
|
|
261
|
-
|
|
262
287
|
if (chCode === COMMA_CODE) {
|
|
263
288
|
index++;
|
|
264
289
|
} else if (chCode !== CCURL_CODE) {
|
|
@@ -282,6 +307,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
282
307
|
__id: ExpNodeOpaqueIdentifier,
|
|
283
308
|
type: 'Object',
|
|
284
309
|
attributes,
|
|
310
|
+
location: getLocation(startCharIndex),
|
|
285
311
|
};
|
|
286
312
|
}
|
|
287
313
|
|
|
@@ -290,7 +316,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
290
316
|
*/
|
|
291
317
|
function gobbleSpaces() {
|
|
292
318
|
let ch = exprICode(index);
|
|
293
|
-
|
|
294
319
|
// Space or tab
|
|
295
320
|
while (ch === 32 || ch === 9) {
|
|
296
321
|
ch = exprICode(++index);
|
|
@@ -303,6 +328,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
303
328
|
function gobbleExpression(): ExpressionNode {
|
|
304
329
|
const test = gobbleBinaryExpression();
|
|
305
330
|
gobbleSpaces();
|
|
331
|
+
const startCharIndex = index;
|
|
306
332
|
|
|
307
333
|
if (index < length && exprICode(index) === QUMARK_CODE) {
|
|
308
334
|
// Ternary expression: test ? consequent : alternate
|
|
@@ -329,6 +355,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
329
355
|
test,
|
|
330
356
|
consequent,
|
|
331
357
|
alternate,
|
|
358
|
+
location: getLocation(startCharIndex),
|
|
332
359
|
};
|
|
333
360
|
}
|
|
334
361
|
|
|
@@ -353,7 +380,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
353
380
|
while (tcLen > 0) {
|
|
354
381
|
if (Object.prototype.hasOwnProperty.call(binaryOps, toCheck)) {
|
|
355
382
|
index += tcLen;
|
|
356
|
-
|
|
357
383
|
return toCheck;
|
|
358
384
|
}
|
|
359
385
|
|
|
@@ -395,7 +421,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
395
421
|
|
|
396
422
|
// Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
|
|
397
423
|
biop = gobbleBinaryOp();
|
|
398
|
-
|
|
399
424
|
while (biop) {
|
|
400
425
|
prec = binaryPrecedence(biop);
|
|
401
426
|
|
|
@@ -410,7 +435,12 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
410
435
|
right = stack.pop();
|
|
411
436
|
biop = stack.pop().value;
|
|
412
437
|
left = stack.pop();
|
|
413
|
-
node = createBinaryExpression(
|
|
438
|
+
node = createBinaryExpression(
|
|
439
|
+
biop,
|
|
440
|
+
left,
|
|
441
|
+
right,
|
|
442
|
+
createSpanningLocation(left.location, right.location)
|
|
443
|
+
);
|
|
414
444
|
stack.push(node);
|
|
415
445
|
}
|
|
416
446
|
|
|
@@ -428,7 +458,12 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
428
458
|
node = stack[i];
|
|
429
459
|
|
|
430
460
|
while (i > 1) {
|
|
431
|
-
node = createBinaryExpression(
|
|
461
|
+
node = createBinaryExpression(
|
|
462
|
+
stack[i - 1].value,
|
|
463
|
+
stack[i - 2],
|
|
464
|
+
node,
|
|
465
|
+
createSpanningLocation(stack[i - 2].location, node.location)
|
|
466
|
+
);
|
|
432
467
|
i -= 2;
|
|
433
468
|
}
|
|
434
469
|
|
|
@@ -442,6 +477,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
442
477
|
function gobbleToken(): any {
|
|
443
478
|
gobbleSpaces();
|
|
444
479
|
const ch = exprICode(index);
|
|
480
|
+
const startCharIndex = index;
|
|
445
481
|
|
|
446
482
|
if (isDecimalDigit(ch) || ch === PERIOD_CODE) {
|
|
447
483
|
// Char code 46 is a dot `.` which can start off a numeric literal
|
|
@@ -478,13 +514,13 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
478
514
|
while (tcLen > 0) {
|
|
479
515
|
if (Object.prototype.hasOwnProperty.call(unaryOps, toCheck)) {
|
|
480
516
|
index += tcLen;
|
|
481
|
-
|
|
482
517
|
return {
|
|
483
518
|
__id: ExpNodeOpaqueIdentifier,
|
|
484
519
|
type: 'UnaryExpression',
|
|
485
520
|
operator: toCheck,
|
|
486
521
|
argument: gobbleToken(),
|
|
487
522
|
prefix: true,
|
|
523
|
+
location: getLocation(startCharIndex),
|
|
488
524
|
};
|
|
489
525
|
}
|
|
490
526
|
|
|
@@ -500,6 +536,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
500
536
|
*/
|
|
501
537
|
function gobbleNumericLiteral() {
|
|
502
538
|
let num = '';
|
|
539
|
+
const startCharIndex = index;
|
|
503
540
|
|
|
504
541
|
while (isDecimalDigit(exprICode(index))) {
|
|
505
542
|
num += exprI(index++);
|
|
@@ -515,7 +552,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
515
552
|
}
|
|
516
553
|
|
|
517
554
|
let ch = exprI(index);
|
|
518
|
-
|
|
519
555
|
if (ch === 'e' || ch === 'E') {
|
|
520
556
|
// Exponent marker
|
|
521
557
|
num += exprI(index++);
|
|
@@ -537,7 +573,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
537
573
|
}
|
|
538
574
|
|
|
539
575
|
const chCode = exprICode(index);
|
|
540
|
-
|
|
541
576
|
// Check to make sure this isn't a variable name that start with a number (123abc)
|
|
542
577
|
if (isIdentifierStart(chCode)) {
|
|
543
578
|
throwError(
|
|
@@ -553,6 +588,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
553
588
|
type: 'Literal',
|
|
554
589
|
value: parseFloat(num),
|
|
555
590
|
raw: num,
|
|
591
|
+
location: getLocation(startCharIndex),
|
|
556
592
|
};
|
|
557
593
|
}
|
|
558
594
|
|
|
@@ -564,6 +600,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
564
600
|
const quote = exprI(index++);
|
|
565
601
|
let str = '';
|
|
566
602
|
let closed = false;
|
|
603
|
+
const startCharIndex = index;
|
|
567
604
|
|
|
568
605
|
while (index < length) {
|
|
569
606
|
let ch = exprI(index++);
|
|
@@ -613,6 +650,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
613
650
|
type: 'Literal',
|
|
614
651
|
value: str,
|
|
615
652
|
raw: `${quote}${str}${quote}`,
|
|
653
|
+
location: getLocation(startCharIndex),
|
|
616
654
|
};
|
|
617
655
|
}
|
|
618
656
|
|
|
@@ -624,9 +662,9 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
624
662
|
let str = '';
|
|
625
663
|
let closed = false;
|
|
626
664
|
let openBraceCount = 1;
|
|
665
|
+
const startCharIndex = index;
|
|
627
666
|
|
|
628
667
|
index += 2; // Skip the {{
|
|
629
|
-
|
|
630
668
|
while (index < length) {
|
|
631
669
|
const ch = exprI(index++);
|
|
632
670
|
|
|
@@ -657,6 +695,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
657
695
|
__id: ExpNodeOpaqueIdentifier,
|
|
658
696
|
type: 'ModelRef',
|
|
659
697
|
ref: str,
|
|
698
|
+
location: getLocation(startCharIndex),
|
|
660
699
|
};
|
|
661
700
|
}
|
|
662
701
|
|
|
@@ -678,7 +717,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
678
717
|
|
|
679
718
|
while (index < length) {
|
|
680
719
|
ch = exprICode(index);
|
|
681
|
-
|
|
682
720
|
if (isIdentifierPart(ch)) {
|
|
683
721
|
index++;
|
|
684
722
|
} else {
|
|
@@ -694,6 +732,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
694
732
|
type: 'Literal',
|
|
695
733
|
value: (literals as any)[identifier],
|
|
696
734
|
raw: identifier,
|
|
735
|
+
location: getLocation(start),
|
|
697
736
|
};
|
|
698
737
|
}
|
|
699
738
|
|
|
@@ -701,6 +740,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
701
740
|
return {
|
|
702
741
|
__id: ExpNodeOpaqueIdentifier,
|
|
703
742
|
type: 'ThisExpression',
|
|
743
|
+
location: getLocation(start),
|
|
704
744
|
};
|
|
705
745
|
}
|
|
706
746
|
|
|
@@ -708,6 +748,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
708
748
|
__id: ExpNodeOpaqueIdentifier,
|
|
709
749
|
type: 'Identifier',
|
|
710
750
|
name: identifier,
|
|
751
|
+
location: getLocation(start),
|
|
711
752
|
};
|
|
712
753
|
}
|
|
713
754
|
|
|
@@ -761,7 +802,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
761
802
|
let charIndex = exprICode(index);
|
|
762
803
|
let node: any =
|
|
763
804
|
charIndex === OPAREN_CODE ? gobbleGroup() : gobbleIdentifier();
|
|
764
|
-
|
|
805
|
+
const startCharIndex = index;
|
|
765
806
|
gobbleSpaces();
|
|
766
807
|
charIndex = exprICode(index);
|
|
767
808
|
|
|
@@ -781,6 +822,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
781
822
|
computed: false,
|
|
782
823
|
object: node,
|
|
783
824
|
property: gobbleIdentifier(),
|
|
825
|
+
location: getLocation(startCharIndex),
|
|
784
826
|
};
|
|
785
827
|
} else if (charIndex === OBRACK_CODE) {
|
|
786
828
|
node = {
|
|
@@ -789,6 +831,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
789
831
|
computed: true,
|
|
790
832
|
object: node,
|
|
791
833
|
property: gobbleExpression(),
|
|
834
|
+
location: getLocation(startCharIndex),
|
|
792
835
|
};
|
|
793
836
|
|
|
794
837
|
gobbleSpaces();
|
|
@@ -806,6 +849,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
806
849
|
type: 'CallExpression',
|
|
807
850
|
args: gobbleArguments(CPAREN_CODE),
|
|
808
851
|
callTarget: node,
|
|
852
|
+
location: getLocation(startCharIndex),
|
|
809
853
|
};
|
|
810
854
|
}
|
|
811
855
|
|
|
@@ -817,7 +861,7 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
817
861
|
}
|
|
818
862
|
|
|
819
863
|
/**
|
|
820
|
-
* Responsible for parsing a group within parentheses `()`
|
|
864
|
+
* Responsible for parsing a group of things within parentheses `()`
|
|
821
865
|
* This function assumes that it needs to gobble the opening parenthesis
|
|
822
866
|
* and then tries to gobble everything within that parenthesis, assuming
|
|
823
867
|
* that the next thing it should see is the close parenthesis. If not,
|
|
@@ -830,7 +874,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
830
874
|
|
|
831
875
|
if (exprICode(index) === CPAREN_CODE) {
|
|
832
876
|
index++;
|
|
833
|
-
|
|
834
877
|
return node;
|
|
835
878
|
}
|
|
836
879
|
|
|
@@ -843,12 +886,14 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
843
886
|
* and then tries to gobble the expressions as arguments.
|
|
844
887
|
*/
|
|
845
888
|
function gobbleArray() {
|
|
889
|
+
const startCharIndex = index;
|
|
846
890
|
index++;
|
|
847
891
|
|
|
848
892
|
return {
|
|
849
893
|
__id: ExpNodeOpaqueIdentifier,
|
|
850
894
|
type: 'ArrayExpression',
|
|
851
895
|
elements: gobbleArguments(CBRACK_CODE),
|
|
896
|
+
location: getLocation(startCharIndex),
|
|
852
897
|
};
|
|
853
898
|
}
|
|
854
899
|
|
|
@@ -885,5 +930,6 @@ export default function parseExpression(expr: string): ExpressionNode {
|
|
|
885
930
|
__id: ExpNodeOpaqueIdentifier,
|
|
886
931
|
type: 'Compound',
|
|
887
932
|
body: nodes,
|
|
933
|
+
location: getLocation(0),
|
|
888
934
|
};
|
|
889
935
|
}
|
package/src/expressions/types.ts
CHANGED
|
@@ -56,12 +56,31 @@ export function isExpressionNode(x: any): x is ExpressionNode {
|
|
|
56
56
|
return typeof x === 'object' && x.__id === ExpNodeOpaqueIdentifier;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface NodePosition {
|
|
60
|
+
/** The character location */
|
|
61
|
+
character: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface NodeLocation {
|
|
65
|
+
// We only care about the character offset, not the line/column for now
|
|
66
|
+
// But making these objects allows us to add more (like line number) later
|
|
67
|
+
|
|
68
|
+
/** The start of the node */
|
|
69
|
+
start: NodePosition;
|
|
70
|
+
|
|
71
|
+
/** The end of the node */
|
|
72
|
+
end: NodePosition;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export interface BaseNode<T> {
|
|
60
76
|
/** The thing to discriminate the AST type on */
|
|
61
77
|
type: T;
|
|
62
78
|
|
|
63
79
|
/** How to tell this apart from other objects */
|
|
64
80
|
__id: typeof ExpNodeOpaqueIdentifier;
|
|
81
|
+
|
|
82
|
+
/** The location of the node in the source expression string */
|
|
83
|
+
location?: NodeLocation;
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
/** A helper interface for nodes that container left and right children */
|
|
@@ -88,13 +107,9 @@ export interface BinaryNode
|
|
|
88
107
|
operator: string;
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
export interface LogicalNode
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/** The right hand side of the equation */
|
|
96
|
-
right: any;
|
|
97
|
-
|
|
110
|
+
export interface LogicalNode
|
|
111
|
+
extends BaseNode<'LogicalExpression'>,
|
|
112
|
+
DirectionalNode {
|
|
98
113
|
/** The logical operation to perform on the nodes */
|
|
99
114
|
operator: string;
|
|
100
115
|
}
|
|
@@ -104,7 +119,7 @@ export interface UnaryNode extends BaseNode<'UnaryExpression'> {
|
|
|
104
119
|
operator: string;
|
|
105
120
|
|
|
106
121
|
/** The single argument that the operation should be performed on */
|
|
107
|
-
argument:
|
|
122
|
+
argument: ExpressionNode;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
export type ThisNode = BaseNode<'ThisExpression'>;
|
|
@@ -118,10 +133,10 @@ export interface ObjectNode extends BaseNode<'Object'> {
|
|
|
118
133
|
/** */
|
|
119
134
|
attributes: Array<{
|
|
120
135
|
/** The property name of the object */
|
|
121
|
-
key:
|
|
136
|
+
key: ExpressionNode;
|
|
122
137
|
|
|
123
138
|
/** the associated value */
|
|
124
|
-
value:
|
|
139
|
+
value: ExpressionNode;
|
|
125
140
|
}>;
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -155,7 +170,7 @@ export interface CompoundNode extends BaseNode<'Compound'> {
|
|
|
155
170
|
|
|
156
171
|
export interface CallExpressionNode extends BaseNode<'CallExpression'> {
|
|
157
172
|
/** The arguments to the function */
|
|
158
|
-
args:
|
|
173
|
+
args: ExpressionNode[];
|
|
159
174
|
|
|
160
175
|
/** The function name */
|
|
161
176
|
callTarget: IdentifierNode;
|
|
@@ -163,7 +178,7 @@ export interface CallExpressionNode extends BaseNode<'CallExpression'> {
|
|
|
163
178
|
|
|
164
179
|
export interface ArrayExpressionNode extends BaseNode<'ArrayExpression'> {
|
|
165
180
|
/** The items in an array */
|
|
166
|
-
elements:
|
|
181
|
+
elements: ExpressionNode[];
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
export interface IdentifierNode extends BaseNode<'Identifier'> {
|
package/src/expressions/utils.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExpressionHandler,
|
|
3
|
+
ExpressionNode,
|
|
4
|
+
NodeLocation,
|
|
5
|
+
NodePosition,
|
|
6
|
+
} from './types';
|
|
2
7
|
|
|
3
8
|
/** Generates a function by removing the first context argument */
|
|
4
9
|
export function withoutContext<T extends unknown[], Return>(
|
|
@@ -6,3 +11,121 @@ export function withoutContext<T extends unknown[], Return>(
|
|
|
6
11
|
): ExpressionHandler<T, Return> {
|
|
7
12
|
return (_context, ...args) => fn(...args);
|
|
8
13
|
}
|
|
14
|
+
|
|
15
|
+
/** Checks if the location includes the target position */
|
|
16
|
+
function isInRange(position: NodePosition, location: NodeLocation) {
|
|
17
|
+
return (
|
|
18
|
+
position.character >= location.start.character &&
|
|
19
|
+
position.character <= location.end.character
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Get the node in the expression that's closest to the desired position */
|
|
24
|
+
export function findClosestNodeAtPosition(
|
|
25
|
+
node: ExpressionNode,
|
|
26
|
+
position: NodePosition
|
|
27
|
+
): ExpressionNode | undefined {
|
|
28
|
+
// This is just mapping recursively over nodes in the tree
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line default-case
|
|
31
|
+
switch (node.type) {
|
|
32
|
+
case 'Modification':
|
|
33
|
+
case 'Assignment':
|
|
34
|
+
case 'LogicalExpression':
|
|
35
|
+
case 'BinaryExpression': {
|
|
36
|
+
const check =
|
|
37
|
+
findClosestNodeAtPosition(node.left, position) ??
|
|
38
|
+
findClosestNodeAtPosition(node.right, position);
|
|
39
|
+
if (check) {
|
|
40
|
+
return check;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case 'UnaryExpression': {
|
|
47
|
+
const checkArg = findClosestNodeAtPosition(node.argument, position);
|
|
48
|
+
if (checkArg) {
|
|
49
|
+
return checkArg;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'MemberExpression': {
|
|
56
|
+
const checkObject =
|
|
57
|
+
findClosestNodeAtPosition(node.object, position) ??
|
|
58
|
+
findClosestNodeAtPosition(node.property, position);
|
|
59
|
+
if (checkObject) {
|
|
60
|
+
return checkObject;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'ConditionalExpression': {
|
|
67
|
+
const checkObject =
|
|
68
|
+
findClosestNodeAtPosition(node.test, position) ??
|
|
69
|
+
findClosestNodeAtPosition(node.consequent, position) ??
|
|
70
|
+
findClosestNodeAtPosition(node.alternate, position);
|
|
71
|
+
if (checkObject) {
|
|
72
|
+
return checkObject;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'ArrayExpression':
|
|
79
|
+
case 'Compound': {
|
|
80
|
+
const elements =
|
|
81
|
+
node.type === 'ArrayExpression' ? node.elements : node.body;
|
|
82
|
+
|
|
83
|
+
const anyElements = elements.find((e) =>
|
|
84
|
+
findClosestNodeAtPosition(e, position)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (anyElements) {
|
|
88
|
+
return anyElements;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'Object': {
|
|
95
|
+
const checkObject = node.attributes.reduce<ExpressionNode | undefined>(
|
|
96
|
+
(found, next) => {
|
|
97
|
+
return (
|
|
98
|
+
found ??
|
|
99
|
+
findClosestNodeAtPosition(next.key, position) ??
|
|
100
|
+
findClosestNodeAtPosition(next.value, position)
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
undefined
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (checkObject) {
|
|
107
|
+
return checkObject;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'CallExpression': {
|
|
114
|
+
const anyArgs =
|
|
115
|
+
node.args.find((arg) => {
|
|
116
|
+
return findClosestNodeAtPosition(arg, position);
|
|
117
|
+
}) ?? findClosestNodeAtPosition(node.callTarget, position);
|
|
118
|
+
|
|
119
|
+
if (anyArgs) {
|
|
120
|
+
return anyArgs;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Lastly check for yourself
|
|
128
|
+
if (node.location && isInRange(position, node.location)) {
|
|
129
|
+
return node;
|
|
130
|
+
}
|
|
131
|
+
}
|