pulse-js-framework 1.4.1 → 1.4.3

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.
@@ -1,1797 +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
- 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
- };
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
+ };