pulse-js-framework 1.2.0 → 1.4.0
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/README.md +414 -245
- package/cli/analyze.js +499 -0
- package/cli/build.js +341 -318
- package/cli/format.js +704 -0
- package/cli/index.js +398 -351
- package/cli/lint.js +642 -0
- package/cli/utils/file-utils.js +298 -0
- package/compiler/lexer.js +766 -598
- package/compiler/parser.js +1797 -900
- package/compiler/transformer.js +1332 -552
- package/index.js +1 -1
- package/package.json +68 -65
- package/runtime/router.js +596 -401
package/compiler/parser.js
CHANGED
|
@@ -1,900 +1,1797 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse Parser - AST builder for .pulse files
|
|
3
|
-
*
|
|
4
|
-
* Converts tokens into an Abstract Syntax Tree
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { TokenType, tokenize } from './lexer.js';
|
|
8
|
-
|
|
9
|
-
// AST Node types
|
|
10
|
-
export const NodeType = {
|
|
11
|
-
Program: 'Program',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (this.is(TokenType.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.expect(TokenType.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
this.
|
|
457
|
-
|
|
458
|
-
this.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
this.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.expect(TokenType.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
*
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
while (this.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return new ASTNode(NodeType.
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Parse
|
|
803
|
-
*/
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Parser - AST builder for .pulse files
|
|
3
|
+
*
|
|
4
|
+
* Converts tokens into an Abstract Syntax Tree
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TokenType, tokenize } from './lexer.js';
|
|
8
|
+
|
|
9
|
+
// AST Node types
|
|
10
|
+
export const NodeType = {
|
|
11
|
+
Program: 'Program',
|
|
12
|
+
ImportDeclaration: 'ImportDeclaration',
|
|
13
|
+
ImportSpecifier: 'ImportSpecifier',
|
|
14
|
+
PageDeclaration: 'PageDeclaration',
|
|
15
|
+
RouteDeclaration: 'RouteDeclaration',
|
|
16
|
+
PropsBlock: 'PropsBlock',
|
|
17
|
+
StateBlock: 'StateBlock',
|
|
18
|
+
ViewBlock: 'ViewBlock',
|
|
19
|
+
ActionsBlock: 'ActionsBlock',
|
|
20
|
+
StyleBlock: 'StyleBlock',
|
|
21
|
+
SlotElement: 'SlotElement',
|
|
22
|
+
Element: 'Element',
|
|
23
|
+
TextNode: 'TextNode',
|
|
24
|
+
Interpolation: 'Interpolation',
|
|
25
|
+
Directive: 'Directive',
|
|
26
|
+
IfDirective: 'IfDirective',
|
|
27
|
+
EachDirective: 'EachDirective',
|
|
28
|
+
EventDirective: 'EventDirective',
|
|
29
|
+
Property: 'Property',
|
|
30
|
+
ObjectLiteral: 'ObjectLiteral',
|
|
31
|
+
ArrayLiteral: 'ArrayLiteral',
|
|
32
|
+
Identifier: 'Identifier',
|
|
33
|
+
MemberExpression: 'MemberExpression',
|
|
34
|
+
CallExpression: 'CallExpression',
|
|
35
|
+
BinaryExpression: 'BinaryExpression',
|
|
36
|
+
UnaryExpression: 'UnaryExpression',
|
|
37
|
+
UpdateExpression: 'UpdateExpression',
|
|
38
|
+
Literal: 'Literal',
|
|
39
|
+
TemplateLiteral: 'TemplateLiteral',
|
|
40
|
+
ConditionalExpression: 'ConditionalExpression',
|
|
41
|
+
ArrowFunction: 'ArrowFunction',
|
|
42
|
+
SpreadElement: 'SpreadElement',
|
|
43
|
+
AssignmentExpression: 'AssignmentExpression',
|
|
44
|
+
FunctionDeclaration: 'FunctionDeclaration',
|
|
45
|
+
StyleRule: 'StyleRule',
|
|
46
|
+
StyleProperty: 'StyleProperty',
|
|
47
|
+
|
|
48
|
+
// Router nodes
|
|
49
|
+
RouterBlock: 'RouterBlock',
|
|
50
|
+
RoutesBlock: 'RoutesBlock',
|
|
51
|
+
RouteDefinition: 'RouteDefinition',
|
|
52
|
+
GuardHook: 'GuardHook',
|
|
53
|
+
|
|
54
|
+
// Store nodes
|
|
55
|
+
StoreBlock: 'StoreBlock',
|
|
56
|
+
GettersBlock: 'GettersBlock',
|
|
57
|
+
GetterDeclaration: 'GetterDeclaration',
|
|
58
|
+
|
|
59
|
+
// View directives for router
|
|
60
|
+
LinkDirective: 'LinkDirective',
|
|
61
|
+
OutletDirective: 'OutletDirective',
|
|
62
|
+
NavigateDirective: 'NavigateDirective'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* AST Node class
|
|
67
|
+
*/
|
|
68
|
+
export class ASTNode {
|
|
69
|
+
constructor(type, props = {}) {
|
|
70
|
+
this.type = type;
|
|
71
|
+
Object.assign(this, props);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parser class
|
|
77
|
+
*/
|
|
78
|
+
export class Parser {
|
|
79
|
+
constructor(tokens) {
|
|
80
|
+
this.tokens = tokens;
|
|
81
|
+
this.pos = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get current token
|
|
86
|
+
*/
|
|
87
|
+
current() {
|
|
88
|
+
return this.tokens[this.pos];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Peek at token at offset
|
|
93
|
+
*/
|
|
94
|
+
peek(offset = 1) {
|
|
95
|
+
return this.tokens[this.pos + offset];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if current token matches type
|
|
100
|
+
*/
|
|
101
|
+
is(type) {
|
|
102
|
+
return this.current()?.type === type;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if current token matches any of types
|
|
107
|
+
*/
|
|
108
|
+
isAny(...types) {
|
|
109
|
+
return types.includes(this.current()?.type);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Advance to next token and return current
|
|
114
|
+
*/
|
|
115
|
+
advance() {
|
|
116
|
+
return this.tokens[this.pos++];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Expect a specific token type
|
|
121
|
+
*/
|
|
122
|
+
expect(type, message = null) {
|
|
123
|
+
if (!this.is(type)) {
|
|
124
|
+
const token = this.current();
|
|
125
|
+
throw new Error(
|
|
126
|
+
message ||
|
|
127
|
+
`Expected ${type} but got ${token?.type} at line ${token?.line}:${token?.column}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return this.advance();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a parse error with detailed information
|
|
135
|
+
*/
|
|
136
|
+
createError(message, token = null) {
|
|
137
|
+
const t = token || this.current();
|
|
138
|
+
const error = new Error(message);
|
|
139
|
+
error.line = t?.line || 1;
|
|
140
|
+
error.column = t?.column || 1;
|
|
141
|
+
error.token = t;
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse the entire program
|
|
147
|
+
*/
|
|
148
|
+
parse() {
|
|
149
|
+
const program = new ASTNode(NodeType.Program, {
|
|
150
|
+
imports: [],
|
|
151
|
+
page: null,
|
|
152
|
+
route: null,
|
|
153
|
+
props: null,
|
|
154
|
+
state: null,
|
|
155
|
+
view: null,
|
|
156
|
+
actions: null,
|
|
157
|
+
style: null,
|
|
158
|
+
router: null,
|
|
159
|
+
store: null
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
while (!this.is(TokenType.EOF)) {
|
|
163
|
+
// Import declarations (must come first)
|
|
164
|
+
if (this.is(TokenType.IMPORT)) {
|
|
165
|
+
program.imports.push(this.parseImportDeclaration());
|
|
166
|
+
}
|
|
167
|
+
// Page/Route declarations
|
|
168
|
+
else if (this.is(TokenType.AT)) {
|
|
169
|
+
this.advance();
|
|
170
|
+
if (this.is(TokenType.PAGE)) {
|
|
171
|
+
program.page = this.parsePageDeclaration();
|
|
172
|
+
} else if (this.is(TokenType.ROUTE)) {
|
|
173
|
+
program.route = this.parseRouteDeclaration();
|
|
174
|
+
} else {
|
|
175
|
+
throw this.createError(
|
|
176
|
+
`Expected 'page' or 'route' after '@', got '${this.current()?.value}'`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Props block
|
|
181
|
+
else if (this.is(TokenType.PROPS)) {
|
|
182
|
+
if (program.props) {
|
|
183
|
+
throw this.createError('Duplicate props block - only one props block allowed per file');
|
|
184
|
+
}
|
|
185
|
+
program.props = this.parsePropsBlock();
|
|
186
|
+
}
|
|
187
|
+
// State block
|
|
188
|
+
else if (this.is(TokenType.STATE)) {
|
|
189
|
+
if (program.state) {
|
|
190
|
+
throw this.createError('Duplicate state block - only one state block allowed per file');
|
|
191
|
+
}
|
|
192
|
+
program.state = this.parseStateBlock();
|
|
193
|
+
}
|
|
194
|
+
// View block
|
|
195
|
+
else if (this.is(TokenType.VIEW)) {
|
|
196
|
+
if (program.view) {
|
|
197
|
+
throw this.createError('Duplicate view block - only one view block allowed per file');
|
|
198
|
+
}
|
|
199
|
+
program.view = this.parseViewBlock();
|
|
200
|
+
}
|
|
201
|
+
// Actions block
|
|
202
|
+
else if (this.is(TokenType.ACTIONS)) {
|
|
203
|
+
if (program.actions) {
|
|
204
|
+
throw this.createError('Duplicate actions block - only one actions block allowed per file');
|
|
205
|
+
}
|
|
206
|
+
program.actions = this.parseActionsBlock();
|
|
207
|
+
}
|
|
208
|
+
// Style block
|
|
209
|
+
else if (this.is(TokenType.STYLE)) {
|
|
210
|
+
if (program.style) {
|
|
211
|
+
throw this.createError('Duplicate style block - only one style block allowed per file');
|
|
212
|
+
}
|
|
213
|
+
program.style = this.parseStyleBlock();
|
|
214
|
+
}
|
|
215
|
+
// Router block
|
|
216
|
+
else if (this.is(TokenType.ROUTER)) {
|
|
217
|
+
if (program.router) {
|
|
218
|
+
throw this.createError('Duplicate router block - only one router block allowed per file');
|
|
219
|
+
}
|
|
220
|
+
program.router = this.parseRouterBlock();
|
|
221
|
+
}
|
|
222
|
+
// Store block
|
|
223
|
+
else if (this.is(TokenType.STORE)) {
|
|
224
|
+
if (program.store) {
|
|
225
|
+
throw this.createError('Duplicate store block - only one store block allowed per file');
|
|
226
|
+
}
|
|
227
|
+
program.store = this.parseStoreBlock();
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const token = this.current();
|
|
231
|
+
throw this.createError(
|
|
232
|
+
`Unexpected token '${token?.value || token?.type}' at line ${token?.line}:${token?.column}. ` +
|
|
233
|
+
`Expected: import, @page, @route, props, state, view, actions, style, router, or store`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return program;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Parse import declaration
|
|
243
|
+
* Supports:
|
|
244
|
+
* import Component from './Component.pulse'
|
|
245
|
+
* import { helper, util } from './utils.pulse'
|
|
246
|
+
* import { helper as h } from './utils.pulse'
|
|
247
|
+
* import * as Utils from './utils.pulse'
|
|
248
|
+
*/
|
|
249
|
+
parseImportDeclaration() {
|
|
250
|
+
const startToken = this.expect(TokenType.IMPORT);
|
|
251
|
+
const specifiers = [];
|
|
252
|
+
let source = null;
|
|
253
|
+
|
|
254
|
+
// import * as Name from '...'
|
|
255
|
+
if (this.is(TokenType.STAR)) {
|
|
256
|
+
this.advance();
|
|
257
|
+
this.expect(TokenType.AS);
|
|
258
|
+
const local = this.expect(TokenType.IDENT);
|
|
259
|
+
specifiers.push(new ASTNode(NodeType.ImportSpecifier, {
|
|
260
|
+
type: 'namespace',
|
|
261
|
+
local: local.value,
|
|
262
|
+
imported: '*'
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
// import { a, b } from '...'
|
|
266
|
+
else if (this.is(TokenType.LBRACE)) {
|
|
267
|
+
this.advance();
|
|
268
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
269
|
+
const imported = this.expect(TokenType.IDENT);
|
|
270
|
+
let local = imported.value;
|
|
271
|
+
|
|
272
|
+
// Handle 'as' alias
|
|
273
|
+
if (this.is(TokenType.AS)) {
|
|
274
|
+
this.advance();
|
|
275
|
+
local = this.expect(TokenType.IDENT).value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
specifiers.push(new ASTNode(NodeType.ImportSpecifier, {
|
|
279
|
+
type: 'named',
|
|
280
|
+
local,
|
|
281
|
+
imported: imported.value
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
if (this.is(TokenType.COMMA)) {
|
|
285
|
+
this.advance();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.expect(TokenType.RBRACE);
|
|
289
|
+
}
|
|
290
|
+
// import DefaultExport from '...'
|
|
291
|
+
else if (this.is(TokenType.IDENT)) {
|
|
292
|
+
const name = this.advance();
|
|
293
|
+
specifiers.push(new ASTNode(NodeType.ImportSpecifier, {
|
|
294
|
+
type: 'default',
|
|
295
|
+
local: name.value,
|
|
296
|
+
imported: 'default'
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// from '...'
|
|
301
|
+
this.expect(TokenType.FROM);
|
|
302
|
+
const sourceToken = this.expect(TokenType.STRING);
|
|
303
|
+
source = sourceToken.value;
|
|
304
|
+
|
|
305
|
+
return new ASTNode(NodeType.ImportDeclaration, {
|
|
306
|
+
specifiers,
|
|
307
|
+
source,
|
|
308
|
+
line: startToken.line,
|
|
309
|
+
column: startToken.column
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parse @page declaration
|
|
315
|
+
*/
|
|
316
|
+
parsePageDeclaration() {
|
|
317
|
+
this.expect(TokenType.PAGE);
|
|
318
|
+
const name = this.expect(TokenType.IDENT);
|
|
319
|
+
return new ASTNode(NodeType.PageDeclaration, { name: name.value });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parse @route declaration
|
|
324
|
+
*/
|
|
325
|
+
parseRouteDeclaration() {
|
|
326
|
+
this.expect(TokenType.ROUTE);
|
|
327
|
+
const path = this.expect(TokenType.STRING);
|
|
328
|
+
return new ASTNode(NodeType.RouteDeclaration, { path: path.value });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Parse props block
|
|
333
|
+
* props {
|
|
334
|
+
* label: "Default"
|
|
335
|
+
* disabled: false
|
|
336
|
+
* }
|
|
337
|
+
*/
|
|
338
|
+
parsePropsBlock() {
|
|
339
|
+
this.expect(TokenType.PROPS);
|
|
340
|
+
this.expect(TokenType.LBRACE);
|
|
341
|
+
|
|
342
|
+
const properties = [];
|
|
343
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
344
|
+
properties.push(this.parsePropsProperty());
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.expect(TokenType.RBRACE);
|
|
348
|
+
return new ASTNode(NodeType.PropsBlock, { properties });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Parse a props property (name: defaultValue)
|
|
353
|
+
*/
|
|
354
|
+
parsePropsProperty() {
|
|
355
|
+
const name = this.expect(TokenType.IDENT);
|
|
356
|
+
this.expect(TokenType.COLON);
|
|
357
|
+
const value = this.parseValue();
|
|
358
|
+
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Parse state block
|
|
363
|
+
*/
|
|
364
|
+
parseStateBlock() {
|
|
365
|
+
this.expect(TokenType.STATE);
|
|
366
|
+
this.expect(TokenType.LBRACE);
|
|
367
|
+
|
|
368
|
+
const properties = [];
|
|
369
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
370
|
+
properties.push(this.parseStateProperty());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.expect(TokenType.RBRACE);
|
|
374
|
+
return new ASTNode(NodeType.StateBlock, { properties });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Parse a state property
|
|
379
|
+
*/
|
|
380
|
+
parseStateProperty() {
|
|
381
|
+
const name = this.expect(TokenType.IDENT);
|
|
382
|
+
this.expect(TokenType.COLON);
|
|
383
|
+
const value = this.parseValue();
|
|
384
|
+
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Parse a value (literal, object, array, etc.)
|
|
389
|
+
*/
|
|
390
|
+
parseValue() {
|
|
391
|
+
if (this.is(TokenType.LBRACE)) {
|
|
392
|
+
return this.parseObjectLiteral();
|
|
393
|
+
}
|
|
394
|
+
if (this.is(TokenType.LBRACKET)) {
|
|
395
|
+
return this.parseArrayLiteral();
|
|
396
|
+
}
|
|
397
|
+
if (this.is(TokenType.STRING)) {
|
|
398
|
+
const token = this.advance();
|
|
399
|
+
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
400
|
+
}
|
|
401
|
+
if (this.is(TokenType.NUMBER)) {
|
|
402
|
+
const token = this.advance();
|
|
403
|
+
return new ASTNode(NodeType.Literal, { value: token.value });
|
|
404
|
+
}
|
|
405
|
+
if (this.is(TokenType.TRUE)) {
|
|
406
|
+
this.advance();
|
|
407
|
+
return new ASTNode(NodeType.Literal, { value: true });
|
|
408
|
+
}
|
|
409
|
+
if (this.is(TokenType.FALSE)) {
|
|
410
|
+
this.advance();
|
|
411
|
+
return new ASTNode(NodeType.Literal, { value: false });
|
|
412
|
+
}
|
|
413
|
+
if (this.is(TokenType.NULL)) {
|
|
414
|
+
this.advance();
|
|
415
|
+
return new ASTNode(NodeType.Literal, { value: null });
|
|
416
|
+
}
|
|
417
|
+
if (this.is(TokenType.IDENT)) {
|
|
418
|
+
return this.parseIdentifierOrExpression();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Parse object literal
|
|
428
|
+
*/
|
|
429
|
+
parseObjectLiteral() {
|
|
430
|
+
this.expect(TokenType.LBRACE);
|
|
431
|
+
const properties = [];
|
|
432
|
+
|
|
433
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
434
|
+
const name = this.expect(TokenType.IDENT);
|
|
435
|
+
this.expect(TokenType.COLON);
|
|
436
|
+
const value = this.parseValue();
|
|
437
|
+
properties.push(new ASTNode(NodeType.Property, { name: name.value, value }));
|
|
438
|
+
|
|
439
|
+
if (this.is(TokenType.COMMA)) {
|
|
440
|
+
this.advance();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.expect(TokenType.RBRACE);
|
|
445
|
+
return new ASTNode(NodeType.ObjectLiteral, { properties });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Parse array literal
|
|
450
|
+
*/
|
|
451
|
+
parseArrayLiteral() {
|
|
452
|
+
this.expect(TokenType.LBRACKET);
|
|
453
|
+
const elements = [];
|
|
454
|
+
|
|
455
|
+
while (!this.is(TokenType.RBRACKET) && !this.is(TokenType.EOF)) {
|
|
456
|
+
elements.push(this.parseValue());
|
|
457
|
+
|
|
458
|
+
if (this.is(TokenType.COMMA)) {
|
|
459
|
+
this.advance();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.expect(TokenType.RBRACKET);
|
|
464
|
+
return new ASTNode(NodeType.ArrayLiteral, { elements });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Parse view block
|
|
469
|
+
*/
|
|
470
|
+
parseViewBlock() {
|
|
471
|
+
this.expect(TokenType.VIEW);
|
|
472
|
+
this.expect(TokenType.LBRACE);
|
|
473
|
+
|
|
474
|
+
const children = [];
|
|
475
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
476
|
+
children.push(this.parseViewChild());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.expect(TokenType.RBRACE);
|
|
480
|
+
return new ASTNode(NodeType.ViewBlock, { children });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Parse a view child (element, directive, slot, or text)
|
|
485
|
+
*/
|
|
486
|
+
parseViewChild() {
|
|
487
|
+
if (this.is(TokenType.AT)) {
|
|
488
|
+
return this.parseDirective();
|
|
489
|
+
}
|
|
490
|
+
// Slot element
|
|
491
|
+
if (this.is(TokenType.SLOT)) {
|
|
492
|
+
return this.parseSlotElement();
|
|
493
|
+
}
|
|
494
|
+
if (this.is(TokenType.SELECTOR) || this.is(TokenType.IDENT)) {
|
|
495
|
+
return this.parseElement();
|
|
496
|
+
}
|
|
497
|
+
if (this.is(TokenType.STRING)) {
|
|
498
|
+
return this.parseTextNode();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const token = this.current();
|
|
502
|
+
throw this.createError(
|
|
503
|
+
`Unexpected token '${token?.value || token?.type}' in view block. ` +
|
|
504
|
+
`Expected: element selector, @directive, slot, or "text"`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Parse slot element for component composition
|
|
510
|
+
* Supports:
|
|
511
|
+
* slot - default slot
|
|
512
|
+
* slot "name" - named slot
|
|
513
|
+
* slot { default content }
|
|
514
|
+
*/
|
|
515
|
+
parseSlotElement() {
|
|
516
|
+
const startToken = this.expect(TokenType.SLOT);
|
|
517
|
+
let name = 'default';
|
|
518
|
+
const fallback = [];
|
|
519
|
+
|
|
520
|
+
// Named slot: slot "header"
|
|
521
|
+
if (this.is(TokenType.STRING)) {
|
|
522
|
+
name = this.advance().value;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Fallback content: slot { ... }
|
|
526
|
+
if (this.is(TokenType.LBRACE)) {
|
|
527
|
+
this.advance();
|
|
528
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
529
|
+
fallback.push(this.parseViewChild());
|
|
530
|
+
}
|
|
531
|
+
this.expect(TokenType.RBRACE);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return new ASTNode(NodeType.SlotElement, {
|
|
535
|
+
name,
|
|
536
|
+
fallback,
|
|
537
|
+
line: startToken.line,
|
|
538
|
+
column: startToken.column
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Parse an element
|
|
544
|
+
*/
|
|
545
|
+
parseElement() {
|
|
546
|
+
const selector = this.isAny(TokenType.SELECTOR, TokenType.IDENT)
|
|
547
|
+
? this.advance().value
|
|
548
|
+
: '';
|
|
549
|
+
|
|
550
|
+
const directives = [];
|
|
551
|
+
const textContent = [];
|
|
552
|
+
const children = [];
|
|
553
|
+
const props = []; // Props passed to component
|
|
554
|
+
|
|
555
|
+
// Check if this is a component with props: Component(prop=value, ...)
|
|
556
|
+
if (this.is(TokenType.LPAREN)) {
|
|
557
|
+
this.advance(); // consume (
|
|
558
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
559
|
+
props.push(this.parseComponentProp());
|
|
560
|
+
if (this.is(TokenType.COMMA)) {
|
|
561
|
+
this.advance();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
this.expect(TokenType.RPAREN);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Parse inline directives and text
|
|
568
|
+
while (!this.is(TokenType.LBRACE) && !this.is(TokenType.RBRACE) &&
|
|
569
|
+
!this.is(TokenType.SELECTOR) && !this.is(TokenType.EOF)) {
|
|
570
|
+
if (this.is(TokenType.AT)) {
|
|
571
|
+
// Check if this is a block directive (@if, @for, @each) - if so, break
|
|
572
|
+
const nextToken = this.peek();
|
|
573
|
+
if (nextToken && (nextToken.type === TokenType.IF ||
|
|
574
|
+
nextToken.type === TokenType.FOR ||
|
|
575
|
+
nextToken.type === TokenType.EACH)) {
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
directives.push(this.parseInlineDirective());
|
|
579
|
+
} else if (this.is(TokenType.STRING)) {
|
|
580
|
+
textContent.push(this.parseTextNode());
|
|
581
|
+
} else if (this.is(TokenType.IDENT) && !this.couldBeElement()) {
|
|
582
|
+
break;
|
|
583
|
+
} else {
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Parse children if there's a block
|
|
589
|
+
if (this.is(TokenType.LBRACE)) {
|
|
590
|
+
this.advance();
|
|
591
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
592
|
+
children.push(this.parseViewChild());
|
|
593
|
+
}
|
|
594
|
+
this.expect(TokenType.RBRACE);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return new ASTNode(NodeType.Element, {
|
|
598
|
+
selector,
|
|
599
|
+
directives,
|
|
600
|
+
textContent,
|
|
601
|
+
children,
|
|
602
|
+
props
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Parse a component prop: name=value or name={expression}
|
|
608
|
+
*/
|
|
609
|
+
parseComponentProp() {
|
|
610
|
+
const name = this.expect(TokenType.IDENT);
|
|
611
|
+
this.expect(TokenType.EQ);
|
|
612
|
+
|
|
613
|
+
let value;
|
|
614
|
+
if (this.is(TokenType.LBRACE)) {
|
|
615
|
+
// Expression prop: name={expression}
|
|
616
|
+
this.advance(); // consume {
|
|
617
|
+
value = this.parseExpression();
|
|
618
|
+
this.expect(TokenType.RBRACE);
|
|
619
|
+
} else if (this.is(TokenType.STRING)) {
|
|
620
|
+
// String prop: name="value"
|
|
621
|
+
const token = this.advance();
|
|
622
|
+
value = new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
623
|
+
} else if (this.is(TokenType.NUMBER)) {
|
|
624
|
+
// Number prop: name=123
|
|
625
|
+
const token = this.advance();
|
|
626
|
+
value = new ASTNode(NodeType.Literal, { value: token.value });
|
|
627
|
+
} else if (this.is(TokenType.TRUE)) {
|
|
628
|
+
this.advance();
|
|
629
|
+
value = new ASTNode(NodeType.Literal, { value: true });
|
|
630
|
+
} else if (this.is(TokenType.FALSE)) {
|
|
631
|
+
this.advance();
|
|
632
|
+
value = new ASTNode(NodeType.Literal, { value: false });
|
|
633
|
+
} else if (this.is(TokenType.NULL)) {
|
|
634
|
+
this.advance();
|
|
635
|
+
value = new ASTNode(NodeType.Literal, { value: null });
|
|
636
|
+
} else if (this.is(TokenType.IDENT)) {
|
|
637
|
+
// Identifier prop: name=someVar
|
|
638
|
+
value = this.parseIdentifierOrExpression();
|
|
639
|
+
} else {
|
|
640
|
+
throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Check if current position could be an element
|
|
648
|
+
*/
|
|
649
|
+
couldBeElement() {
|
|
650
|
+
const next = this.peek();
|
|
651
|
+
return next?.type === TokenType.LBRACE ||
|
|
652
|
+
next?.type === TokenType.AT ||
|
|
653
|
+
next?.type === TokenType.STRING;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Parse a text node
|
|
658
|
+
*/
|
|
659
|
+
parseTextNode() {
|
|
660
|
+
const token = this.expect(TokenType.STRING);
|
|
661
|
+
const parts = this.parseInterpolatedString(token.value);
|
|
662
|
+
return new ASTNode(NodeType.TextNode, { parts });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Parse interpolated string into parts
|
|
667
|
+
* "Hello, {name}!" -> ["Hello, ", { expr: "name" }, "!"]
|
|
668
|
+
*/
|
|
669
|
+
parseInterpolatedString(str) {
|
|
670
|
+
const parts = [];
|
|
671
|
+
let current = '';
|
|
672
|
+
let i = 0;
|
|
673
|
+
|
|
674
|
+
while (i < str.length) {
|
|
675
|
+
if (str[i] === '{') {
|
|
676
|
+
if (current) {
|
|
677
|
+
parts.push(current);
|
|
678
|
+
current = '';
|
|
679
|
+
}
|
|
680
|
+
i++; // skip {
|
|
681
|
+
let expr = '';
|
|
682
|
+
let braceCount = 1;
|
|
683
|
+
while (i < str.length && braceCount > 0) {
|
|
684
|
+
if (str[i] === '{') braceCount++;
|
|
685
|
+
else if (str[i] === '}') braceCount--;
|
|
686
|
+
if (braceCount > 0) expr += str[i];
|
|
687
|
+
i++;
|
|
688
|
+
}
|
|
689
|
+
parts.push(new ASTNode(NodeType.Interpolation, { expression: expr.trim() }));
|
|
690
|
+
} else {
|
|
691
|
+
current += str[i];
|
|
692
|
+
i++;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (current) {
|
|
697
|
+
parts.push(current);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return parts;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Parse a directive (@if, @for, @each, @click, @link, @outlet, @navigate, etc.)
|
|
705
|
+
*/
|
|
706
|
+
parseDirective() {
|
|
707
|
+
this.expect(TokenType.AT);
|
|
708
|
+
|
|
709
|
+
// Handle @if - IF is a keyword token, not IDENT
|
|
710
|
+
if (this.is(TokenType.IF)) {
|
|
711
|
+
this.advance();
|
|
712
|
+
return this.parseIfDirective();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Handle @for - FOR is a keyword token, not IDENT
|
|
716
|
+
if (this.is(TokenType.FOR)) {
|
|
717
|
+
this.advance();
|
|
718
|
+
return this.parseEachDirective();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Handle router directives
|
|
722
|
+
if (this.is(TokenType.LINK)) {
|
|
723
|
+
this.advance();
|
|
724
|
+
return this.parseLinkDirective();
|
|
725
|
+
}
|
|
726
|
+
if (this.is(TokenType.OUTLET)) {
|
|
727
|
+
this.advance();
|
|
728
|
+
return this.parseOutletDirective();
|
|
729
|
+
}
|
|
730
|
+
if (this.is(TokenType.NAVIGATE)) {
|
|
731
|
+
this.advance();
|
|
732
|
+
return this.parseNavigateDirective();
|
|
733
|
+
}
|
|
734
|
+
if (this.is(TokenType.BACK)) {
|
|
735
|
+
this.advance();
|
|
736
|
+
return new ASTNode(NodeType.NavigateDirective, { action: 'back' });
|
|
737
|
+
}
|
|
738
|
+
if (this.is(TokenType.FORWARD)) {
|
|
739
|
+
this.advance();
|
|
740
|
+
return new ASTNode(NodeType.NavigateDirective, { action: 'forward' });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
744
|
+
|
|
745
|
+
if (name === 'if') {
|
|
746
|
+
return this.parseIfDirective();
|
|
747
|
+
}
|
|
748
|
+
if (name === 'each' || name === 'for') {
|
|
749
|
+
return this.parseEachDirective();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Event directive like @click
|
|
753
|
+
return this.parseEventDirective(name);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Parse inline directive
|
|
758
|
+
*/
|
|
759
|
+
parseInlineDirective() {
|
|
760
|
+
this.expect(TokenType.AT);
|
|
761
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
762
|
+
|
|
763
|
+
// Event directive
|
|
764
|
+
this.expect(TokenType.LPAREN);
|
|
765
|
+
const expression = this.parseExpression();
|
|
766
|
+
this.expect(TokenType.RPAREN);
|
|
767
|
+
|
|
768
|
+
return new ASTNode(NodeType.EventDirective, { event: name, handler: expression });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Parse @if directive
|
|
773
|
+
*/
|
|
774
|
+
parseIfDirective() {
|
|
775
|
+
this.expect(TokenType.LPAREN);
|
|
776
|
+
const condition = this.parseExpression();
|
|
777
|
+
this.expect(TokenType.RPAREN);
|
|
778
|
+
|
|
779
|
+
this.expect(TokenType.LBRACE);
|
|
780
|
+
const consequent = [];
|
|
781
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
782
|
+
consequent.push(this.parseViewChild());
|
|
783
|
+
}
|
|
784
|
+
this.expect(TokenType.RBRACE);
|
|
785
|
+
|
|
786
|
+
let alternate = null;
|
|
787
|
+
if (this.is(TokenType.AT) && this.peek()?.value === 'else') {
|
|
788
|
+
this.advance(); // @
|
|
789
|
+
this.advance(); // else
|
|
790
|
+
this.expect(TokenType.LBRACE);
|
|
791
|
+
alternate = [];
|
|
792
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
793
|
+
alternate.push(this.parseViewChild());
|
|
794
|
+
}
|
|
795
|
+
this.expect(TokenType.RBRACE);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return new ASTNode(NodeType.IfDirective, { condition, consequent, alternate });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Parse @each/@for directive
|
|
803
|
+
*/
|
|
804
|
+
parseEachDirective() {
|
|
805
|
+
this.expect(TokenType.LPAREN);
|
|
806
|
+
const itemName = this.expect(TokenType.IDENT).value;
|
|
807
|
+
// Accept both 'in' and 'of' keywords
|
|
808
|
+
if (this.is(TokenType.IN)) {
|
|
809
|
+
this.advance();
|
|
810
|
+
} else if (this.is(TokenType.OF)) {
|
|
811
|
+
this.advance();
|
|
812
|
+
} else {
|
|
813
|
+
throw this.createError('Expected "in" or "of" in loop directive');
|
|
814
|
+
}
|
|
815
|
+
const iterable = this.parseExpression();
|
|
816
|
+
this.expect(TokenType.RPAREN);
|
|
817
|
+
|
|
818
|
+
this.expect(TokenType.LBRACE);
|
|
819
|
+
const template = [];
|
|
820
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
821
|
+
template.push(this.parseViewChild());
|
|
822
|
+
}
|
|
823
|
+
this.expect(TokenType.RBRACE);
|
|
824
|
+
|
|
825
|
+
return new ASTNode(NodeType.EachDirective, { itemName, iterable, template });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Parse event directive
|
|
830
|
+
*/
|
|
831
|
+
parseEventDirective(event) {
|
|
832
|
+
this.expect(TokenType.LPAREN);
|
|
833
|
+
const handler = this.parseExpression();
|
|
834
|
+
this.expect(TokenType.RPAREN);
|
|
835
|
+
|
|
836
|
+
const children = [];
|
|
837
|
+
if (this.is(TokenType.LBRACE)) {
|
|
838
|
+
this.advance();
|
|
839
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
840
|
+
children.push(this.parseViewChild());
|
|
841
|
+
}
|
|
842
|
+
this.expect(TokenType.RBRACE);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return new ASTNode(NodeType.EventDirective, { event, handler, children });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Parse expression
|
|
850
|
+
*/
|
|
851
|
+
parseExpression() {
|
|
852
|
+
return this.parseAssignmentExpression();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Parse assignment expression (a = b)
|
|
857
|
+
*/
|
|
858
|
+
parseAssignmentExpression() {
|
|
859
|
+
const left = this.parseConditionalExpression();
|
|
860
|
+
|
|
861
|
+
if (this.is(TokenType.EQ)) {
|
|
862
|
+
this.advance();
|
|
863
|
+
const right = this.parseAssignmentExpression();
|
|
864
|
+
return new ASTNode(NodeType.AssignmentExpression, { left, right });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return left;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Parse conditional (ternary) expression
|
|
872
|
+
*/
|
|
873
|
+
parseConditionalExpression() {
|
|
874
|
+
const test = this.parseOrExpression();
|
|
875
|
+
|
|
876
|
+
if (this.is(TokenType.QUESTION)) {
|
|
877
|
+
this.advance();
|
|
878
|
+
const consequent = this.parseAssignmentExpression();
|
|
879
|
+
this.expect(TokenType.COLON);
|
|
880
|
+
const alternate = this.parseAssignmentExpression();
|
|
881
|
+
return new ASTNode(NodeType.ConditionalExpression, { test, consequent, alternate });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return test;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Parse OR expression
|
|
889
|
+
*/
|
|
890
|
+
parseOrExpression() {
|
|
891
|
+
let left = this.parseAndExpression();
|
|
892
|
+
|
|
893
|
+
while (this.is(TokenType.OR)) {
|
|
894
|
+
this.advance();
|
|
895
|
+
const right = this.parseAndExpression();
|
|
896
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator: '||', left, right });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return left;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Parse AND expression
|
|
904
|
+
*/
|
|
905
|
+
parseAndExpression() {
|
|
906
|
+
let left = this.parseComparisonExpression();
|
|
907
|
+
|
|
908
|
+
while (this.is(TokenType.AND)) {
|
|
909
|
+
this.advance();
|
|
910
|
+
const right = this.parseComparisonExpression();
|
|
911
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator: '&&', left, right });
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return left;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Parse comparison expression
|
|
919
|
+
*/
|
|
920
|
+
parseComparisonExpression() {
|
|
921
|
+
let left = this.parseAdditiveExpression();
|
|
922
|
+
|
|
923
|
+
while (this.isAny(TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
|
|
924
|
+
TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE)) {
|
|
925
|
+
const operator = this.advance().value;
|
|
926
|
+
const right = this.parseAdditiveExpression();
|
|
927
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return left;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Parse additive expression
|
|
935
|
+
*/
|
|
936
|
+
parseAdditiveExpression() {
|
|
937
|
+
let left = this.parseMultiplicativeExpression();
|
|
938
|
+
|
|
939
|
+
while (this.isAny(TokenType.PLUS, TokenType.MINUS)) {
|
|
940
|
+
const operator = this.advance().value;
|
|
941
|
+
const right = this.parseMultiplicativeExpression();
|
|
942
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return left;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Parse multiplicative expression
|
|
950
|
+
*/
|
|
951
|
+
parseMultiplicativeExpression() {
|
|
952
|
+
let left = this.parseUnaryExpression();
|
|
953
|
+
|
|
954
|
+
while (this.isAny(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
|
|
955
|
+
const operator = this.advance().value;
|
|
956
|
+
const right = this.parseUnaryExpression();
|
|
957
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return left;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Parse unary expression
|
|
965
|
+
*/
|
|
966
|
+
parseUnaryExpression() {
|
|
967
|
+
if (this.is(TokenType.NOT)) {
|
|
968
|
+
this.advance();
|
|
969
|
+
const argument = this.parseUnaryExpression();
|
|
970
|
+
return new ASTNode(NodeType.UnaryExpression, { operator: '!', argument });
|
|
971
|
+
}
|
|
972
|
+
if (this.is(TokenType.MINUS)) {
|
|
973
|
+
this.advance();
|
|
974
|
+
const argument = this.parseUnaryExpression();
|
|
975
|
+
return new ASTNode(NodeType.UnaryExpression, { operator: '-', argument });
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return this.parsePostfixExpression();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Parse postfix expression (++, --)
|
|
983
|
+
*/
|
|
984
|
+
parsePostfixExpression() {
|
|
985
|
+
let expr = this.parsePrimaryExpression();
|
|
986
|
+
|
|
987
|
+
while (this.isAny(TokenType.PLUSPLUS, TokenType.MINUSMINUS)) {
|
|
988
|
+
const operator = this.advance().value;
|
|
989
|
+
expr = new ASTNode(NodeType.UpdateExpression, {
|
|
990
|
+
operator,
|
|
991
|
+
argument: expr,
|
|
992
|
+
prefix: false
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return expr;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Parse primary expression
|
|
1001
|
+
*/
|
|
1002
|
+
parsePrimaryExpression() {
|
|
1003
|
+
// Check for arrow function: (params) => expr or () => expr
|
|
1004
|
+
if (this.is(TokenType.LPAREN)) {
|
|
1005
|
+
// Try to parse as arrow function by looking ahead
|
|
1006
|
+
const savedPos = this.pos;
|
|
1007
|
+
if (this.tryParseArrowFunction()) {
|
|
1008
|
+
this.pos = savedPos;
|
|
1009
|
+
return this.parseArrowFunction();
|
|
1010
|
+
}
|
|
1011
|
+
// Not an arrow function, parse as grouped expression
|
|
1012
|
+
this.advance();
|
|
1013
|
+
const expr = this.parseExpression();
|
|
1014
|
+
this.expect(TokenType.RPAREN);
|
|
1015
|
+
// Check if this grouped expression is actually arrow function params
|
|
1016
|
+
if (this.is(TokenType.ARROW)) {
|
|
1017
|
+
this.pos = savedPos;
|
|
1018
|
+
return this.parseArrowFunction();
|
|
1019
|
+
}
|
|
1020
|
+
return expr;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Single param arrow function: x => expr
|
|
1024
|
+
if ((this.is(TokenType.IDENT) || this.is(TokenType.SELECTOR)) && this.peek()?.type === TokenType.ARROW) {
|
|
1025
|
+
return this.parseArrowFunction();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Array literal
|
|
1029
|
+
if (this.is(TokenType.LBRACKET)) {
|
|
1030
|
+
return this.parseArrayLiteralExpr();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Object literal in expression context
|
|
1034
|
+
if (this.is(TokenType.LBRACE)) {
|
|
1035
|
+
return this.parseObjectLiteralExpr();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Template literal
|
|
1039
|
+
if (this.is(TokenType.TEMPLATE)) {
|
|
1040
|
+
const token = this.advance();
|
|
1041
|
+
return new ASTNode(NodeType.TemplateLiteral, { value: token.value, raw: token.raw });
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Spread operator
|
|
1045
|
+
if (this.is(TokenType.SPREAD)) {
|
|
1046
|
+
this.advance();
|
|
1047
|
+
const argument = this.parseAssignmentExpression();
|
|
1048
|
+
return new ASTNode(NodeType.SpreadElement, { argument });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (this.is(TokenType.NUMBER)) {
|
|
1052
|
+
const token = this.advance();
|
|
1053
|
+
return new ASTNode(NodeType.Literal, { value: token.value });
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (this.is(TokenType.STRING)) {
|
|
1057
|
+
const token = this.advance();
|
|
1058
|
+
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (this.is(TokenType.TRUE)) {
|
|
1062
|
+
this.advance();
|
|
1063
|
+
return new ASTNode(NodeType.Literal, { value: true });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (this.is(TokenType.FALSE)) {
|
|
1067
|
+
this.advance();
|
|
1068
|
+
return new ASTNode(NodeType.Literal, { value: false });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (this.is(TokenType.NULL)) {
|
|
1072
|
+
this.advance();
|
|
1073
|
+
return new ASTNode(NodeType.Literal, { value: null });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// In expressions, SELECTOR tokens should be treated as IDENT
|
|
1077
|
+
// This happens when identifiers like 'selectedCategory' are followed by space in view context
|
|
1078
|
+
if (this.is(TokenType.IDENT) || this.is(TokenType.SELECTOR)) {
|
|
1079
|
+
return this.parseIdentifierOrExpression();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Unexpected token ${this.current()?.type} in expression at line ${this.current()?.line}`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Try to determine if we're looking at an arrow function
|
|
1089
|
+
*/
|
|
1090
|
+
tryParseArrowFunction() {
|
|
1091
|
+
if (!this.is(TokenType.LPAREN)) return false;
|
|
1092
|
+
|
|
1093
|
+
let depth = 0;
|
|
1094
|
+
let i = 0;
|
|
1095
|
+
|
|
1096
|
+
while (this.peek(i)) {
|
|
1097
|
+
const token = this.peek(i);
|
|
1098
|
+
if (token.type === TokenType.LPAREN) depth++;
|
|
1099
|
+
else if (token.type === TokenType.RPAREN) {
|
|
1100
|
+
depth--;
|
|
1101
|
+
if (depth === 0) {
|
|
1102
|
+
// Check if next token is =>
|
|
1103
|
+
const next = this.peek(i + 1);
|
|
1104
|
+
return next?.type === TokenType.ARROW;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
i++;
|
|
1108
|
+
}
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Parse arrow function: (params) => expr or param => expr
|
|
1114
|
+
*/
|
|
1115
|
+
parseArrowFunction() {
|
|
1116
|
+
const params = [];
|
|
1117
|
+
|
|
1118
|
+
// Single param without parens: x => expr
|
|
1119
|
+
if ((this.is(TokenType.IDENT) || this.is(TokenType.SELECTOR)) && this.peek()?.type === TokenType.ARROW) {
|
|
1120
|
+
params.push(this.advance().value);
|
|
1121
|
+
} else {
|
|
1122
|
+
// Params in parens: (a, b) => expr or () => expr
|
|
1123
|
+
this.expect(TokenType.LPAREN);
|
|
1124
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
1125
|
+
if (this.is(TokenType.SPREAD)) {
|
|
1126
|
+
this.advance();
|
|
1127
|
+
params.push('...' + this.expect(TokenType.IDENT).value);
|
|
1128
|
+
} else {
|
|
1129
|
+
params.push(this.expect(TokenType.IDENT).value);
|
|
1130
|
+
}
|
|
1131
|
+
if (this.is(TokenType.COMMA)) {
|
|
1132
|
+
this.advance();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
this.expect(TokenType.RPAREN);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
this.expect(TokenType.ARROW);
|
|
1139
|
+
|
|
1140
|
+
// Body can be expression or block
|
|
1141
|
+
let body;
|
|
1142
|
+
if (this.is(TokenType.LBRACE)) {
|
|
1143
|
+
// Block body - collect tokens
|
|
1144
|
+
this.advance();
|
|
1145
|
+
body = this.parseFunctionBody();
|
|
1146
|
+
this.expect(TokenType.RBRACE);
|
|
1147
|
+
return new ASTNode(NodeType.ArrowFunction, { params, body, block: true });
|
|
1148
|
+
} else {
|
|
1149
|
+
// Expression body
|
|
1150
|
+
body = this.parseAssignmentExpression();
|
|
1151
|
+
return new ASTNode(NodeType.ArrowFunction, { params, body, block: false });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Parse array literal in expression context
|
|
1157
|
+
*/
|
|
1158
|
+
parseArrayLiteralExpr() {
|
|
1159
|
+
this.expect(TokenType.LBRACKET);
|
|
1160
|
+
const elements = [];
|
|
1161
|
+
|
|
1162
|
+
while (!this.is(TokenType.RBRACKET) && !this.is(TokenType.EOF)) {
|
|
1163
|
+
if (this.is(TokenType.SPREAD)) {
|
|
1164
|
+
this.advance();
|
|
1165
|
+
elements.push(new ASTNode(NodeType.SpreadElement, {
|
|
1166
|
+
argument: this.parseAssignmentExpression()
|
|
1167
|
+
}));
|
|
1168
|
+
} else {
|
|
1169
|
+
elements.push(this.parseAssignmentExpression());
|
|
1170
|
+
}
|
|
1171
|
+
if (this.is(TokenType.COMMA)) {
|
|
1172
|
+
this.advance();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
this.expect(TokenType.RBRACKET);
|
|
1177
|
+
return new ASTNode(NodeType.ArrayLiteral, { elements });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Parse object literal in expression context
|
|
1182
|
+
*/
|
|
1183
|
+
parseObjectLiteralExpr() {
|
|
1184
|
+
this.expect(TokenType.LBRACE);
|
|
1185
|
+
const properties = [];
|
|
1186
|
+
|
|
1187
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1188
|
+
if (this.is(TokenType.SPREAD)) {
|
|
1189
|
+
this.advance();
|
|
1190
|
+
properties.push(new ASTNode(NodeType.SpreadElement, {
|
|
1191
|
+
argument: this.parseAssignmentExpression()
|
|
1192
|
+
}));
|
|
1193
|
+
} else {
|
|
1194
|
+
const key = this.expect(TokenType.IDENT);
|
|
1195
|
+
if (this.is(TokenType.COLON)) {
|
|
1196
|
+
this.advance();
|
|
1197
|
+
const value = this.parseAssignmentExpression();
|
|
1198
|
+
properties.push(new ASTNode(NodeType.Property, { name: key.value, value }));
|
|
1199
|
+
} else {
|
|
1200
|
+
// Shorthand property: { x } is same as { x: x }
|
|
1201
|
+
properties.push(new ASTNode(NodeType.Property, {
|
|
1202
|
+
name: key.value,
|
|
1203
|
+
value: new ASTNode(NodeType.Identifier, { name: key.value }),
|
|
1204
|
+
shorthand: true
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (this.is(TokenType.COMMA)) {
|
|
1209
|
+
this.advance();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
this.expect(TokenType.RBRACE);
|
|
1214
|
+
return new ASTNode(NodeType.ObjectLiteral, { properties });
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Parse identifier with possible member access and calls
|
|
1219
|
+
*/
|
|
1220
|
+
parseIdentifierOrExpression() {
|
|
1221
|
+
// Accept both IDENT and SELECTOR (selector tokens can be identifiers in expression context)
|
|
1222
|
+
const token = this.advance();
|
|
1223
|
+
let expr = new ASTNode(NodeType.Identifier, { name: token.value });
|
|
1224
|
+
|
|
1225
|
+
while (true) {
|
|
1226
|
+
if (this.is(TokenType.DOT)) {
|
|
1227
|
+
this.advance();
|
|
1228
|
+
const property = this.expect(TokenType.IDENT);
|
|
1229
|
+
expr = new ASTNode(NodeType.MemberExpression, {
|
|
1230
|
+
object: expr,
|
|
1231
|
+
property: property.value
|
|
1232
|
+
});
|
|
1233
|
+
} else if (this.is(TokenType.LBRACKET)) {
|
|
1234
|
+
this.advance();
|
|
1235
|
+
const property = this.parseExpression();
|
|
1236
|
+
this.expect(TokenType.RBRACKET);
|
|
1237
|
+
expr = new ASTNode(NodeType.MemberExpression, {
|
|
1238
|
+
object: expr,
|
|
1239
|
+
property,
|
|
1240
|
+
computed: true
|
|
1241
|
+
});
|
|
1242
|
+
} else if (this.is(TokenType.LPAREN)) {
|
|
1243
|
+
this.advance();
|
|
1244
|
+
const args = [];
|
|
1245
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
1246
|
+
args.push(this.parseExpression());
|
|
1247
|
+
if (this.is(TokenType.COMMA)) {
|
|
1248
|
+
this.advance();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
this.expect(TokenType.RPAREN);
|
|
1252
|
+
expr = new ASTNode(NodeType.CallExpression, { callee: expr, arguments: args });
|
|
1253
|
+
} else {
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return expr;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Parse actions block
|
|
1263
|
+
*/
|
|
1264
|
+
parseActionsBlock() {
|
|
1265
|
+
this.expect(TokenType.ACTIONS);
|
|
1266
|
+
this.expect(TokenType.LBRACE);
|
|
1267
|
+
|
|
1268
|
+
const functions = [];
|
|
1269
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1270
|
+
functions.push(this.parseFunctionDeclaration());
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
this.expect(TokenType.RBRACE);
|
|
1274
|
+
return new ASTNode(NodeType.ActionsBlock, { functions });
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Parse function declaration
|
|
1279
|
+
*/
|
|
1280
|
+
parseFunctionDeclaration() {
|
|
1281
|
+
let async = false;
|
|
1282
|
+
if (this.is(TokenType.IDENT) && this.current().value === 'async') {
|
|
1283
|
+
this.advance();
|
|
1284
|
+
async = true;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
1288
|
+
this.expect(TokenType.LPAREN);
|
|
1289
|
+
|
|
1290
|
+
const params = [];
|
|
1291
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
1292
|
+
// Accept IDENT or keyword tokens that can be used as parameter names
|
|
1293
|
+
const paramToken = this.current();
|
|
1294
|
+
if (this.is(TokenType.IDENT) || this.is(TokenType.PAGE) ||
|
|
1295
|
+
this.is(TokenType.ROUTE) || this.is(TokenType.FROM) ||
|
|
1296
|
+
this.is(TokenType.STATE) || this.is(TokenType.VIEW) ||
|
|
1297
|
+
this.is(TokenType.STORE) || this.is(TokenType.ROUTER)) {
|
|
1298
|
+
params.push(this.advance().value);
|
|
1299
|
+
} else {
|
|
1300
|
+
throw this.createError(`Expected parameter name but got ${paramToken?.type}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (this.is(TokenType.COMMA)) {
|
|
1303
|
+
this.advance();
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
this.expect(TokenType.RPAREN);
|
|
1307
|
+
|
|
1308
|
+
// Parse function body as raw JS
|
|
1309
|
+
this.expect(TokenType.LBRACE);
|
|
1310
|
+
const body = this.parseFunctionBody();
|
|
1311
|
+
this.expect(TokenType.RBRACE);
|
|
1312
|
+
|
|
1313
|
+
return new ASTNode(NodeType.FunctionDeclaration, { name, params, body, async });
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Parse function body (raw content between braces)
|
|
1318
|
+
*/
|
|
1319
|
+
parseFunctionBody() {
|
|
1320
|
+
// Simplified: collect all tokens until matching }
|
|
1321
|
+
const statements = [];
|
|
1322
|
+
let braceCount = 1;
|
|
1323
|
+
|
|
1324
|
+
while (!this.is(TokenType.EOF)) {
|
|
1325
|
+
if (this.is(TokenType.LBRACE)) {
|
|
1326
|
+
braceCount++;
|
|
1327
|
+
} else if (this.is(TokenType.RBRACE)) {
|
|
1328
|
+
braceCount--;
|
|
1329
|
+
if (braceCount === 0) break;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Collect raw token for reconstruction
|
|
1333
|
+
statements.push(this.current());
|
|
1334
|
+
this.advance();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return statements;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Parse style block
|
|
1342
|
+
*/
|
|
1343
|
+
parseStyleBlock() {
|
|
1344
|
+
this.expect(TokenType.STYLE);
|
|
1345
|
+
this.expect(TokenType.LBRACE);
|
|
1346
|
+
|
|
1347
|
+
const rules = [];
|
|
1348
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1349
|
+
rules.push(this.parseStyleRule());
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
this.expect(TokenType.RBRACE);
|
|
1353
|
+
return new ASTNode(NodeType.StyleBlock, { rules });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Parse style rule
|
|
1358
|
+
*/
|
|
1359
|
+
parseStyleRule() {
|
|
1360
|
+
// Parse selector
|
|
1361
|
+
let selector = '';
|
|
1362
|
+
while (!this.is(TokenType.LBRACE) && !this.is(TokenType.EOF)) {
|
|
1363
|
+
selector += this.advance().value;
|
|
1364
|
+
}
|
|
1365
|
+
selector = selector.trim();
|
|
1366
|
+
|
|
1367
|
+
this.expect(TokenType.LBRACE);
|
|
1368
|
+
|
|
1369
|
+
const properties = [];
|
|
1370
|
+
const nestedRules = [];
|
|
1371
|
+
|
|
1372
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1373
|
+
// Check if this is a nested rule or a property
|
|
1374
|
+
if (this.isNestedRule()) {
|
|
1375
|
+
nestedRules.push(this.parseStyleRule());
|
|
1376
|
+
} else {
|
|
1377
|
+
properties.push(this.parseStyleProperty());
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
this.expect(TokenType.RBRACE);
|
|
1382
|
+
return new ASTNode(NodeType.StyleRule, { selector, properties, nestedRules });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Check if current position is a nested rule
|
|
1387
|
+
*/
|
|
1388
|
+
isNestedRule() {
|
|
1389
|
+
// Look ahead to see if there's a { before a : or newline
|
|
1390
|
+
let i = 0;
|
|
1391
|
+
while (this.peek(i) && this.peek(i).type !== TokenType.EOF) {
|
|
1392
|
+
const token = this.peek(i);
|
|
1393
|
+
if (token.type === TokenType.LBRACE) return true;
|
|
1394
|
+
if (token.type === TokenType.COLON) return false;
|
|
1395
|
+
if (token.type === TokenType.RBRACE) return false;
|
|
1396
|
+
i++;
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Parse style property
|
|
1403
|
+
*/
|
|
1404
|
+
parseStyleProperty() {
|
|
1405
|
+
let name = '';
|
|
1406
|
+
while (!this.is(TokenType.COLON) && !this.is(TokenType.EOF)) {
|
|
1407
|
+
name += this.advance().value;
|
|
1408
|
+
}
|
|
1409
|
+
name = name.trim();
|
|
1410
|
+
|
|
1411
|
+
this.expect(TokenType.COLON);
|
|
1412
|
+
|
|
1413
|
+
// Tokens that should not have space after them in CSS values
|
|
1414
|
+
const noSpaceAfter = new Set(['#', '(', '.', '/', 'rgba', 'rgb', 'hsl', 'hsla', 'var', 'calc', 'url', 'linear-gradient', 'radial-gradient']);
|
|
1415
|
+
// Tokens that should not have space before them
|
|
1416
|
+
const noSpaceBefore = new Set([')', ',', '%', 'px', 'em', 'rem', 'vh', 'vw', 'fr', 's', 'ms', '(']);
|
|
1417
|
+
|
|
1418
|
+
let value = '';
|
|
1419
|
+
let lastTokenValue = '';
|
|
1420
|
+
let afterHash = false; // Track if we're collecting a hex color
|
|
1421
|
+
let inCssVar = false; // Track if we're inside var(--...)
|
|
1422
|
+
|
|
1423
|
+
while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) &&
|
|
1424
|
+
!this.is(TokenType.EOF) && !this.isPropertyStart()) {
|
|
1425
|
+
const token = this.advance();
|
|
1426
|
+
// Use raw value if available to preserve original representation
|
|
1427
|
+
// This is important for numbers that might be parsed as scientific notation
|
|
1428
|
+
let tokenValue = token.raw || String(token.value);
|
|
1429
|
+
|
|
1430
|
+
// Track CSS var() context - no spaces in var(--name)
|
|
1431
|
+
if (lastTokenValue === 'var' && tokenValue === '(') {
|
|
1432
|
+
inCssVar = true;
|
|
1433
|
+
} else if (inCssVar && tokenValue === ')') {
|
|
1434
|
+
inCssVar = false;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// For hex colors (#abc123), collect tokens without spacing after #
|
|
1438
|
+
if (tokenValue === '#') {
|
|
1439
|
+
afterHash = true;
|
|
1440
|
+
} else if (afterHash) {
|
|
1441
|
+
// Still collecting hex color - no space
|
|
1442
|
+
// Stop collecting when we hit a space-requiring character
|
|
1443
|
+
if (tokenValue === ' ' || tokenValue === ';' || tokenValue === ')') {
|
|
1444
|
+
afterHash = false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Add space before this token unless:
|
|
1449
|
+
// - It's the first token
|
|
1450
|
+
// - Last token was in noSpaceAfter
|
|
1451
|
+
// - This token is in noSpaceBefore
|
|
1452
|
+
// - We're collecting a hex color (afterHash and last was #)
|
|
1453
|
+
// - We're inside var(--...) and this is part of the variable name
|
|
1454
|
+
// - Last was '-' and current is an identifier (hyphenated name)
|
|
1455
|
+
const skipSpace = noSpaceAfter.has(lastTokenValue) ||
|
|
1456
|
+
noSpaceBefore.has(tokenValue) ||
|
|
1457
|
+
(afterHash && lastTokenValue === '#') ||
|
|
1458
|
+
(afterHash && /^[0-9a-fA-F]+$/.test(tokenValue)) ||
|
|
1459
|
+
inCssVar ||
|
|
1460
|
+
(lastTokenValue === '-' || lastTokenValue === '--') ||
|
|
1461
|
+
(tokenValue === '-' && /^[a-zA-Z]/.test(this.current()?.value || ''));
|
|
1462
|
+
|
|
1463
|
+
if (value.length > 0 && !skipSpace) {
|
|
1464
|
+
value += ' ';
|
|
1465
|
+
afterHash = false; // Space ends hex collection
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
value += tokenValue;
|
|
1469
|
+
lastTokenValue = tokenValue;
|
|
1470
|
+
}
|
|
1471
|
+
value = value.trim();
|
|
1472
|
+
|
|
1473
|
+
if (this.is(TokenType.SEMICOLON)) {
|
|
1474
|
+
this.advance();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return new ASTNode(NodeType.StyleProperty, { name, value });
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// =============================================================================
|
|
1481
|
+
// Router Parsing
|
|
1482
|
+
// =============================================================================
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Parse router block
|
|
1486
|
+
* router {
|
|
1487
|
+
* mode: "hash"
|
|
1488
|
+
* base: "/app"
|
|
1489
|
+
* routes { "/": HomePage }
|
|
1490
|
+
* beforeEach(to, from) { ... }
|
|
1491
|
+
* afterEach(to) { ... }
|
|
1492
|
+
* }
|
|
1493
|
+
*/
|
|
1494
|
+
parseRouterBlock() {
|
|
1495
|
+
this.expect(TokenType.ROUTER);
|
|
1496
|
+
this.expect(TokenType.LBRACE);
|
|
1497
|
+
|
|
1498
|
+
const config = {
|
|
1499
|
+
mode: 'history',
|
|
1500
|
+
base: '',
|
|
1501
|
+
routes: [],
|
|
1502
|
+
beforeEach: null,
|
|
1503
|
+
afterEach: null
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1507
|
+
// mode: "hash"
|
|
1508
|
+
if (this.is(TokenType.MODE)) {
|
|
1509
|
+
this.advance();
|
|
1510
|
+
this.expect(TokenType.COLON);
|
|
1511
|
+
config.mode = this.expect(TokenType.STRING).value;
|
|
1512
|
+
}
|
|
1513
|
+
// base: "/app"
|
|
1514
|
+
else if (this.is(TokenType.BASE)) {
|
|
1515
|
+
this.advance();
|
|
1516
|
+
this.expect(TokenType.COLON);
|
|
1517
|
+
config.base = this.expect(TokenType.STRING).value;
|
|
1518
|
+
}
|
|
1519
|
+
// routes { ... }
|
|
1520
|
+
else if (this.is(TokenType.ROUTES)) {
|
|
1521
|
+
config.routes = this.parseRoutesBlock();
|
|
1522
|
+
}
|
|
1523
|
+
// beforeEach(to, from) { ... }
|
|
1524
|
+
else if (this.is(TokenType.BEFORE_EACH)) {
|
|
1525
|
+
config.beforeEach = this.parseGuardHook('beforeEach');
|
|
1526
|
+
}
|
|
1527
|
+
// afterEach(to) { ... }
|
|
1528
|
+
else if (this.is(TokenType.AFTER_EACH)) {
|
|
1529
|
+
config.afterEach = this.parseGuardHook('afterEach');
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
throw this.createError(
|
|
1533
|
+
`Unexpected token '${this.current()?.value}' in router block. ` +
|
|
1534
|
+
`Expected: mode, base, routes, beforeEach, or afterEach`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
this.expect(TokenType.RBRACE);
|
|
1540
|
+
return new ASTNode(NodeType.RouterBlock, config);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* Parse routes block
|
|
1545
|
+
* routes {
|
|
1546
|
+
* "/": HomePage
|
|
1547
|
+
* "/users/:id": UserPage
|
|
1548
|
+
* }
|
|
1549
|
+
*/
|
|
1550
|
+
parseRoutesBlock() {
|
|
1551
|
+
this.expect(TokenType.ROUTES);
|
|
1552
|
+
this.expect(TokenType.LBRACE);
|
|
1553
|
+
|
|
1554
|
+
const routes = [];
|
|
1555
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1556
|
+
const path = this.expect(TokenType.STRING).value;
|
|
1557
|
+
this.expect(TokenType.COLON);
|
|
1558
|
+
const handler = this.expect(TokenType.IDENT).value;
|
|
1559
|
+
routes.push(new ASTNode(NodeType.RouteDefinition, { path, handler }));
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this.expect(TokenType.RBRACE);
|
|
1563
|
+
return routes;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Parse guard hook: beforeEach(to, from) { ... }
|
|
1568
|
+
*/
|
|
1569
|
+
parseGuardHook(name) {
|
|
1570
|
+
this.advance(); // skip keyword
|
|
1571
|
+
this.expect(TokenType.LPAREN);
|
|
1572
|
+
const params = [];
|
|
1573
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
1574
|
+
// Accept IDENT or FROM (since 'from' is a keyword but valid as parameter name)
|
|
1575
|
+
if (this.is(TokenType.IDENT)) {
|
|
1576
|
+
params.push(this.advance().value);
|
|
1577
|
+
} else if (this.is(TokenType.FROM)) {
|
|
1578
|
+
params.push(this.advance().value);
|
|
1579
|
+
} else {
|
|
1580
|
+
throw this.createError(`Expected parameter name but got ${this.current()?.type}`);
|
|
1581
|
+
}
|
|
1582
|
+
if (this.is(TokenType.COMMA)) this.advance();
|
|
1583
|
+
}
|
|
1584
|
+
this.expect(TokenType.RPAREN);
|
|
1585
|
+
this.expect(TokenType.LBRACE);
|
|
1586
|
+
const body = this.parseFunctionBody();
|
|
1587
|
+
this.expect(TokenType.RBRACE);
|
|
1588
|
+
|
|
1589
|
+
return new ASTNode(NodeType.GuardHook, { name, params, body });
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// =============================================================================
|
|
1593
|
+
// Store Parsing
|
|
1594
|
+
// =============================================================================
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Parse store block
|
|
1598
|
+
* store {
|
|
1599
|
+
* state { ... }
|
|
1600
|
+
* getters { ... }
|
|
1601
|
+
* actions { ... }
|
|
1602
|
+
* persist: true
|
|
1603
|
+
* storageKey: "my-store"
|
|
1604
|
+
* }
|
|
1605
|
+
*/
|
|
1606
|
+
parseStoreBlock() {
|
|
1607
|
+
this.expect(TokenType.STORE);
|
|
1608
|
+
this.expect(TokenType.LBRACE);
|
|
1609
|
+
|
|
1610
|
+
const config = {
|
|
1611
|
+
state: null,
|
|
1612
|
+
getters: null,
|
|
1613
|
+
actions: null,
|
|
1614
|
+
persist: false,
|
|
1615
|
+
storageKey: 'pulse-store',
|
|
1616
|
+
plugins: []
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1620
|
+
// state { ... }
|
|
1621
|
+
if (this.is(TokenType.STATE)) {
|
|
1622
|
+
config.state = this.parseStateBlock();
|
|
1623
|
+
}
|
|
1624
|
+
// getters { ... }
|
|
1625
|
+
else if (this.is(TokenType.GETTERS)) {
|
|
1626
|
+
config.getters = this.parseGettersBlock();
|
|
1627
|
+
}
|
|
1628
|
+
// actions { ... }
|
|
1629
|
+
else if (this.is(TokenType.ACTIONS)) {
|
|
1630
|
+
config.actions = this.parseActionsBlock();
|
|
1631
|
+
}
|
|
1632
|
+
// persist: true
|
|
1633
|
+
else if (this.is(TokenType.PERSIST)) {
|
|
1634
|
+
this.advance();
|
|
1635
|
+
this.expect(TokenType.COLON);
|
|
1636
|
+
if (this.is(TokenType.TRUE)) {
|
|
1637
|
+
this.advance();
|
|
1638
|
+
config.persist = true;
|
|
1639
|
+
} else if (this.is(TokenType.FALSE)) {
|
|
1640
|
+
this.advance();
|
|
1641
|
+
config.persist = false;
|
|
1642
|
+
} else {
|
|
1643
|
+
throw this.createError('Expected true or false for persist');
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// storageKey: "my-store"
|
|
1647
|
+
else if (this.is(TokenType.STORAGE_KEY)) {
|
|
1648
|
+
this.advance();
|
|
1649
|
+
this.expect(TokenType.COLON);
|
|
1650
|
+
config.storageKey = this.expect(TokenType.STRING).value;
|
|
1651
|
+
}
|
|
1652
|
+
// plugins: [historyPlugin, loggerPlugin]
|
|
1653
|
+
else if (this.is(TokenType.PLUGINS)) {
|
|
1654
|
+
this.advance();
|
|
1655
|
+
this.expect(TokenType.COLON);
|
|
1656
|
+
config.plugins = this.parseArrayLiteral();
|
|
1657
|
+
}
|
|
1658
|
+
else {
|
|
1659
|
+
throw this.createError(
|
|
1660
|
+
`Unexpected token '${this.current()?.value}' in store block. ` +
|
|
1661
|
+
`Expected: state, getters, actions, persist, storageKey, or plugins`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
this.expect(TokenType.RBRACE);
|
|
1667
|
+
return new ASTNode(NodeType.StoreBlock, config);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Parse getters block
|
|
1672
|
+
* getters {
|
|
1673
|
+
* doubled() { return this.count * 2 }
|
|
1674
|
+
* }
|
|
1675
|
+
*/
|
|
1676
|
+
parseGettersBlock() {
|
|
1677
|
+
this.expect(TokenType.GETTERS);
|
|
1678
|
+
this.expect(TokenType.LBRACE);
|
|
1679
|
+
|
|
1680
|
+
const getters = [];
|
|
1681
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1682
|
+
getters.push(this.parseGetterDeclaration());
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
this.expect(TokenType.RBRACE);
|
|
1686
|
+
return new ASTNode(NodeType.GettersBlock, { getters });
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Parse getter declaration: name() { return ... }
|
|
1691
|
+
*/
|
|
1692
|
+
parseGetterDeclaration() {
|
|
1693
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
1694
|
+
this.expect(TokenType.LPAREN);
|
|
1695
|
+
this.expect(TokenType.RPAREN);
|
|
1696
|
+
this.expect(TokenType.LBRACE);
|
|
1697
|
+
const body = this.parseFunctionBody();
|
|
1698
|
+
this.expect(TokenType.RBRACE);
|
|
1699
|
+
|
|
1700
|
+
return new ASTNode(NodeType.GetterDeclaration, { name, body });
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// =============================================================================
|
|
1704
|
+
// Router View Directives
|
|
1705
|
+
// =============================================================================
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Parse @link directive: @link("/path") "text"
|
|
1709
|
+
*/
|
|
1710
|
+
parseLinkDirective() {
|
|
1711
|
+
this.expect(TokenType.LPAREN);
|
|
1712
|
+
const path = this.parseExpression();
|
|
1713
|
+
|
|
1714
|
+
let options = null;
|
|
1715
|
+
if (this.is(TokenType.COMMA)) {
|
|
1716
|
+
this.advance();
|
|
1717
|
+
options = this.parseObjectLiteralExpr();
|
|
1718
|
+
}
|
|
1719
|
+
this.expect(TokenType.RPAREN);
|
|
1720
|
+
|
|
1721
|
+
// Parse link content (text or children)
|
|
1722
|
+
let content = null;
|
|
1723
|
+
if (this.is(TokenType.STRING)) {
|
|
1724
|
+
content = this.parseTextNode();
|
|
1725
|
+
} else if (this.is(TokenType.LBRACE)) {
|
|
1726
|
+
this.advance();
|
|
1727
|
+
content = [];
|
|
1728
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1729
|
+
content.push(this.parseViewChild());
|
|
1730
|
+
}
|
|
1731
|
+
this.expect(TokenType.RBRACE);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return new ASTNode(NodeType.LinkDirective, { path, options, content });
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Parse @outlet directive
|
|
1739
|
+
*/
|
|
1740
|
+
parseOutletDirective() {
|
|
1741
|
+
let container = null;
|
|
1742
|
+
if (this.is(TokenType.LPAREN)) {
|
|
1743
|
+
this.advance();
|
|
1744
|
+
if (this.is(TokenType.STRING)) {
|
|
1745
|
+
container = this.expect(TokenType.STRING).value;
|
|
1746
|
+
}
|
|
1747
|
+
this.expect(TokenType.RPAREN);
|
|
1748
|
+
}
|
|
1749
|
+
return new ASTNode(NodeType.OutletDirective, { container });
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* Parse @navigate directive
|
|
1754
|
+
*/
|
|
1755
|
+
parseNavigateDirective() {
|
|
1756
|
+
this.expect(TokenType.LPAREN);
|
|
1757
|
+
const path = this.parseExpression();
|
|
1758
|
+
|
|
1759
|
+
let options = null;
|
|
1760
|
+
if (this.is(TokenType.COMMA)) {
|
|
1761
|
+
this.advance();
|
|
1762
|
+
options = this.parseObjectLiteralExpr();
|
|
1763
|
+
}
|
|
1764
|
+
this.expect(TokenType.RPAREN);
|
|
1765
|
+
|
|
1766
|
+
return new ASTNode(NodeType.NavigateDirective, { path, options });
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Check if current position starts a new property
|
|
1771
|
+
*/
|
|
1772
|
+
isPropertyStart() {
|
|
1773
|
+
// Check if it looks like: identifier followed by :
|
|
1774
|
+
if (!this.is(TokenType.IDENT)) return false;
|
|
1775
|
+
let i = 1;
|
|
1776
|
+
while (this.peek(i) && this.peek(i).type === TokenType.IDENT) {
|
|
1777
|
+
i++;
|
|
1778
|
+
}
|
|
1779
|
+
return this.peek(i)?.type === TokenType.COLON;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Parse source code into AST
|
|
1785
|
+
*/
|
|
1786
|
+
export function parse(source) {
|
|
1787
|
+
const tokens = tokenize(source);
|
|
1788
|
+
const parser = new Parser(tokens);
|
|
1789
|
+
return parser.parse();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
export default {
|
|
1793
|
+
NodeType,
|
|
1794
|
+
ASTNode,
|
|
1795
|
+
Parser,
|
|
1796
|
+
parse
|
|
1797
|
+
};
|