pulse-js-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/cli/build.js +199 -0
- package/cli/dev.js +225 -0
- package/cli/index.js +324 -0
- package/compiler/index.js +65 -0
- package/compiler/lexer.js +581 -0
- package/compiler/parser.js +900 -0
- package/compiler/transformer.js +552 -0
- package/index.js +19 -0
- package/loader/vite-plugin.js +160 -0
- package/package.json +58 -0
- package/runtime/dom.js +484 -0
- package/runtime/index.js +13 -0
- package/runtime/pulse.js +339 -0
- package/runtime/router.js +392 -0
- package/runtime/store.js +301 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Parser - AST builder for .pulse files
|
|
3
|
+
*
|
|
4
|
+
* Converts tokens into an Abstract Syntax Tree
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TokenType, tokenize } from './lexer.js';
|
|
8
|
+
|
|
9
|
+
// AST Node types
|
|
10
|
+
export const NodeType = {
|
|
11
|
+
Program: 'Program',
|
|
12
|
+
PageDeclaration: 'PageDeclaration',
|
|
13
|
+
RouteDeclaration: 'RouteDeclaration',
|
|
14
|
+
StateBlock: 'StateBlock',
|
|
15
|
+
ViewBlock: 'ViewBlock',
|
|
16
|
+
ActionsBlock: 'ActionsBlock',
|
|
17
|
+
StyleBlock: 'StyleBlock',
|
|
18
|
+
Element: 'Element',
|
|
19
|
+
TextNode: 'TextNode',
|
|
20
|
+
Interpolation: 'Interpolation',
|
|
21
|
+
Directive: 'Directive',
|
|
22
|
+
IfDirective: 'IfDirective',
|
|
23
|
+
EachDirective: 'EachDirective',
|
|
24
|
+
EventDirective: 'EventDirective',
|
|
25
|
+
Property: 'Property',
|
|
26
|
+
ObjectLiteral: 'ObjectLiteral',
|
|
27
|
+
ArrayLiteral: 'ArrayLiteral',
|
|
28
|
+
Identifier: 'Identifier',
|
|
29
|
+
MemberExpression: 'MemberExpression',
|
|
30
|
+
CallExpression: 'CallExpression',
|
|
31
|
+
BinaryExpression: 'BinaryExpression',
|
|
32
|
+
UnaryExpression: 'UnaryExpression',
|
|
33
|
+
UpdateExpression: 'UpdateExpression',
|
|
34
|
+
Literal: 'Literal',
|
|
35
|
+
FunctionDeclaration: 'FunctionDeclaration',
|
|
36
|
+
StyleRule: 'StyleRule',
|
|
37
|
+
StyleProperty: 'StyleProperty'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* AST Node class
|
|
42
|
+
*/
|
|
43
|
+
export class ASTNode {
|
|
44
|
+
constructor(type, props = {}) {
|
|
45
|
+
this.type = type;
|
|
46
|
+
Object.assign(this, props);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parser class
|
|
52
|
+
*/
|
|
53
|
+
export class Parser {
|
|
54
|
+
constructor(tokens) {
|
|
55
|
+
this.tokens = tokens;
|
|
56
|
+
this.pos = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get current token
|
|
61
|
+
*/
|
|
62
|
+
current() {
|
|
63
|
+
return this.tokens[this.pos];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Peek at token at offset
|
|
68
|
+
*/
|
|
69
|
+
peek(offset = 1) {
|
|
70
|
+
return this.tokens[this.pos + offset];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if current token matches type
|
|
75
|
+
*/
|
|
76
|
+
is(type) {
|
|
77
|
+
return this.current()?.type === type;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if current token matches any of types
|
|
82
|
+
*/
|
|
83
|
+
isAny(...types) {
|
|
84
|
+
return types.includes(this.current()?.type);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Advance to next token and return current
|
|
89
|
+
*/
|
|
90
|
+
advance() {
|
|
91
|
+
return this.tokens[this.pos++];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Expect a specific token type
|
|
96
|
+
*/
|
|
97
|
+
expect(type, message = null) {
|
|
98
|
+
if (!this.is(type)) {
|
|
99
|
+
const token = this.current();
|
|
100
|
+
throw new Error(
|
|
101
|
+
message ||
|
|
102
|
+
`Expected ${type} but got ${token?.type} at line ${token?.line}:${token?.column}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return this.advance();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse the entire program
|
|
110
|
+
*/
|
|
111
|
+
parse() {
|
|
112
|
+
const program = new ASTNode(NodeType.Program, {
|
|
113
|
+
page: null,
|
|
114
|
+
route: null,
|
|
115
|
+
state: null,
|
|
116
|
+
view: null,
|
|
117
|
+
actions: null,
|
|
118
|
+
style: null
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
while (!this.is(TokenType.EOF)) {
|
|
122
|
+
if (this.is(TokenType.AT)) {
|
|
123
|
+
this.advance();
|
|
124
|
+
if (this.is(TokenType.PAGE)) {
|
|
125
|
+
program.page = this.parsePageDeclaration();
|
|
126
|
+
} else if (this.is(TokenType.ROUTE)) {
|
|
127
|
+
program.route = this.parseRouteDeclaration();
|
|
128
|
+
}
|
|
129
|
+
} else if (this.is(TokenType.STATE)) {
|
|
130
|
+
program.state = this.parseStateBlock();
|
|
131
|
+
} else if (this.is(TokenType.VIEW)) {
|
|
132
|
+
program.view = this.parseViewBlock();
|
|
133
|
+
} else if (this.is(TokenType.ACTIONS)) {
|
|
134
|
+
program.actions = this.parseActionsBlock();
|
|
135
|
+
} else if (this.is(TokenType.STYLE)) {
|
|
136
|
+
program.style = this.parseStyleBlock();
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Unexpected token ${this.current()?.type} at line ${this.current()?.line}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return program;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse @page declaration
|
|
149
|
+
*/
|
|
150
|
+
parsePageDeclaration() {
|
|
151
|
+
this.expect(TokenType.PAGE);
|
|
152
|
+
const name = this.expect(TokenType.IDENT);
|
|
153
|
+
return new ASTNode(NodeType.PageDeclaration, { name: name.value });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse @route declaration
|
|
158
|
+
*/
|
|
159
|
+
parseRouteDeclaration() {
|
|
160
|
+
this.expect(TokenType.ROUTE);
|
|
161
|
+
const path = this.expect(TokenType.STRING);
|
|
162
|
+
return new ASTNode(NodeType.RouteDeclaration, { path: path.value });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse state block
|
|
167
|
+
*/
|
|
168
|
+
parseStateBlock() {
|
|
169
|
+
this.expect(TokenType.STATE);
|
|
170
|
+
this.expect(TokenType.LBRACE);
|
|
171
|
+
|
|
172
|
+
const properties = [];
|
|
173
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
174
|
+
properties.push(this.parseStateProperty());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.expect(TokenType.RBRACE);
|
|
178
|
+
return new ASTNode(NodeType.StateBlock, { properties });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse a state property
|
|
183
|
+
*/
|
|
184
|
+
parseStateProperty() {
|
|
185
|
+
const name = this.expect(TokenType.IDENT);
|
|
186
|
+
this.expect(TokenType.COLON);
|
|
187
|
+
const value = this.parseValue();
|
|
188
|
+
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse a value (literal, object, array, etc.)
|
|
193
|
+
*/
|
|
194
|
+
parseValue() {
|
|
195
|
+
if (this.is(TokenType.LBRACE)) {
|
|
196
|
+
return this.parseObjectLiteral();
|
|
197
|
+
}
|
|
198
|
+
if (this.is(TokenType.LBRACKET)) {
|
|
199
|
+
return this.parseArrayLiteral();
|
|
200
|
+
}
|
|
201
|
+
if (this.is(TokenType.STRING)) {
|
|
202
|
+
const token = this.advance();
|
|
203
|
+
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
204
|
+
}
|
|
205
|
+
if (this.is(TokenType.NUMBER)) {
|
|
206
|
+
const token = this.advance();
|
|
207
|
+
return new ASTNode(NodeType.Literal, { value: token.value });
|
|
208
|
+
}
|
|
209
|
+
if (this.is(TokenType.TRUE)) {
|
|
210
|
+
this.advance();
|
|
211
|
+
return new ASTNode(NodeType.Literal, { value: true });
|
|
212
|
+
}
|
|
213
|
+
if (this.is(TokenType.FALSE)) {
|
|
214
|
+
this.advance();
|
|
215
|
+
return new ASTNode(NodeType.Literal, { value: false });
|
|
216
|
+
}
|
|
217
|
+
if (this.is(TokenType.NULL)) {
|
|
218
|
+
this.advance();
|
|
219
|
+
return new ASTNode(NodeType.Literal, { value: null });
|
|
220
|
+
}
|
|
221
|
+
if (this.is(TokenType.IDENT)) {
|
|
222
|
+
return this.parseIdentifierOrExpression();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse object literal
|
|
232
|
+
*/
|
|
233
|
+
parseObjectLiteral() {
|
|
234
|
+
this.expect(TokenType.LBRACE);
|
|
235
|
+
const properties = [];
|
|
236
|
+
|
|
237
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
238
|
+
const name = this.expect(TokenType.IDENT);
|
|
239
|
+
this.expect(TokenType.COLON);
|
|
240
|
+
const value = this.parseValue();
|
|
241
|
+
properties.push(new ASTNode(NodeType.Property, { name: name.value, value }));
|
|
242
|
+
|
|
243
|
+
if (this.is(TokenType.COMMA)) {
|
|
244
|
+
this.advance();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.expect(TokenType.RBRACE);
|
|
249
|
+
return new ASTNode(NodeType.ObjectLiteral, { properties });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parse array literal
|
|
254
|
+
*/
|
|
255
|
+
parseArrayLiteral() {
|
|
256
|
+
this.expect(TokenType.LBRACKET);
|
|
257
|
+
const elements = [];
|
|
258
|
+
|
|
259
|
+
while (!this.is(TokenType.RBRACKET) && !this.is(TokenType.EOF)) {
|
|
260
|
+
elements.push(this.parseValue());
|
|
261
|
+
|
|
262
|
+
if (this.is(TokenType.COMMA)) {
|
|
263
|
+
this.advance();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.expect(TokenType.RBRACKET);
|
|
268
|
+
return new ASTNode(NodeType.ArrayLiteral, { elements });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Parse view block
|
|
273
|
+
*/
|
|
274
|
+
parseViewBlock() {
|
|
275
|
+
this.expect(TokenType.VIEW);
|
|
276
|
+
this.expect(TokenType.LBRACE);
|
|
277
|
+
|
|
278
|
+
const children = [];
|
|
279
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
280
|
+
children.push(this.parseViewChild());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.expect(TokenType.RBRACE);
|
|
284
|
+
return new ASTNode(NodeType.ViewBlock, { children });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse a view child (element, directive, or text)
|
|
289
|
+
*/
|
|
290
|
+
parseViewChild() {
|
|
291
|
+
if (this.is(TokenType.AT)) {
|
|
292
|
+
return this.parseDirective();
|
|
293
|
+
}
|
|
294
|
+
if (this.is(TokenType.SELECTOR) || this.is(TokenType.IDENT)) {
|
|
295
|
+
return this.parseElement();
|
|
296
|
+
}
|
|
297
|
+
if (this.is(TokenType.STRING)) {
|
|
298
|
+
return this.parseTextNode();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Unexpected token ${this.current()?.type} in view at line ${this.current()?.line}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Parse an element
|
|
308
|
+
*/
|
|
309
|
+
parseElement() {
|
|
310
|
+
const selector = this.isAny(TokenType.SELECTOR, TokenType.IDENT)
|
|
311
|
+
? this.advance().value
|
|
312
|
+
: '';
|
|
313
|
+
|
|
314
|
+
const directives = [];
|
|
315
|
+
const textContent = [];
|
|
316
|
+
const children = [];
|
|
317
|
+
|
|
318
|
+
// Parse inline directives and text
|
|
319
|
+
while (!this.is(TokenType.LBRACE) && !this.is(TokenType.RBRACE) &&
|
|
320
|
+
!this.is(TokenType.SELECTOR) && !this.is(TokenType.EOF)) {
|
|
321
|
+
if (this.is(TokenType.AT)) {
|
|
322
|
+
directives.push(this.parseInlineDirective());
|
|
323
|
+
} else if (this.is(TokenType.STRING)) {
|
|
324
|
+
textContent.push(this.parseTextNode());
|
|
325
|
+
} else if (this.is(TokenType.IDENT) && !this.couldBeElement()) {
|
|
326
|
+
break;
|
|
327
|
+
} else {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Parse children if there's a block
|
|
333
|
+
if (this.is(TokenType.LBRACE)) {
|
|
334
|
+
this.advance();
|
|
335
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
336
|
+
children.push(this.parseViewChild());
|
|
337
|
+
}
|
|
338
|
+
this.expect(TokenType.RBRACE);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return new ASTNode(NodeType.Element, {
|
|
342
|
+
selector,
|
|
343
|
+
directives,
|
|
344
|
+
textContent,
|
|
345
|
+
children
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if current position could be an element
|
|
351
|
+
*/
|
|
352
|
+
couldBeElement() {
|
|
353
|
+
const next = this.peek();
|
|
354
|
+
return next?.type === TokenType.LBRACE ||
|
|
355
|
+
next?.type === TokenType.AT ||
|
|
356
|
+
next?.type === TokenType.STRING;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Parse a text node
|
|
361
|
+
*/
|
|
362
|
+
parseTextNode() {
|
|
363
|
+
const token = this.expect(TokenType.STRING);
|
|
364
|
+
const parts = this.parseInterpolatedString(token.value);
|
|
365
|
+
return new ASTNode(NodeType.TextNode, { parts });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Parse interpolated string into parts
|
|
370
|
+
* "Hello, {name}!" -> ["Hello, ", { expr: "name" }, "!"]
|
|
371
|
+
*/
|
|
372
|
+
parseInterpolatedString(str) {
|
|
373
|
+
const parts = [];
|
|
374
|
+
let current = '';
|
|
375
|
+
let i = 0;
|
|
376
|
+
|
|
377
|
+
while (i < str.length) {
|
|
378
|
+
if (str[i] === '{') {
|
|
379
|
+
if (current) {
|
|
380
|
+
parts.push(current);
|
|
381
|
+
current = '';
|
|
382
|
+
}
|
|
383
|
+
i++; // skip {
|
|
384
|
+
let expr = '';
|
|
385
|
+
let braceCount = 1;
|
|
386
|
+
while (i < str.length && braceCount > 0) {
|
|
387
|
+
if (str[i] === '{') braceCount++;
|
|
388
|
+
else if (str[i] === '}') braceCount--;
|
|
389
|
+
if (braceCount > 0) expr += str[i];
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
parts.push(new ASTNode(NodeType.Interpolation, { expression: expr.trim() }));
|
|
393
|
+
} else {
|
|
394
|
+
current += str[i];
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (current) {
|
|
400
|
+
parts.push(current);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return parts;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Parse a directive (@if, @each, @click, etc.)
|
|
408
|
+
*/
|
|
409
|
+
parseDirective() {
|
|
410
|
+
this.expect(TokenType.AT);
|
|
411
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
412
|
+
|
|
413
|
+
if (name === 'if') {
|
|
414
|
+
return this.parseIfDirective();
|
|
415
|
+
}
|
|
416
|
+
if (name === 'each') {
|
|
417
|
+
return this.parseEachDirective();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Event directive like @click
|
|
421
|
+
return this.parseEventDirective(name);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Parse inline directive
|
|
426
|
+
*/
|
|
427
|
+
parseInlineDirective() {
|
|
428
|
+
this.expect(TokenType.AT);
|
|
429
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
430
|
+
|
|
431
|
+
// Event directive
|
|
432
|
+
this.expect(TokenType.LPAREN);
|
|
433
|
+
const expression = this.parseExpression();
|
|
434
|
+
this.expect(TokenType.RPAREN);
|
|
435
|
+
|
|
436
|
+
return new ASTNode(NodeType.EventDirective, { event: name, handler: expression });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Parse @if directive
|
|
441
|
+
*/
|
|
442
|
+
parseIfDirective() {
|
|
443
|
+
this.expect(TokenType.LPAREN);
|
|
444
|
+
const condition = this.parseExpression();
|
|
445
|
+
this.expect(TokenType.RPAREN);
|
|
446
|
+
|
|
447
|
+
this.expect(TokenType.LBRACE);
|
|
448
|
+
const consequent = [];
|
|
449
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
450
|
+
consequent.push(this.parseViewChild());
|
|
451
|
+
}
|
|
452
|
+
this.expect(TokenType.RBRACE);
|
|
453
|
+
|
|
454
|
+
let alternate = null;
|
|
455
|
+
if (this.is(TokenType.AT) && this.peek()?.value === 'else') {
|
|
456
|
+
this.advance(); // @
|
|
457
|
+
this.advance(); // else
|
|
458
|
+
this.expect(TokenType.LBRACE);
|
|
459
|
+
alternate = [];
|
|
460
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
461
|
+
alternate.push(this.parseViewChild());
|
|
462
|
+
}
|
|
463
|
+
this.expect(TokenType.RBRACE);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return new ASTNode(NodeType.IfDirective, { condition, consequent, alternate });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Parse @each directive
|
|
471
|
+
*/
|
|
472
|
+
parseEachDirective() {
|
|
473
|
+
this.expect(TokenType.LPAREN);
|
|
474
|
+
const itemName = this.expect(TokenType.IDENT).value;
|
|
475
|
+
this.expect(TokenType.IN);
|
|
476
|
+
const iterable = this.parseExpression();
|
|
477
|
+
this.expect(TokenType.RPAREN);
|
|
478
|
+
|
|
479
|
+
this.expect(TokenType.LBRACE);
|
|
480
|
+
const template = [];
|
|
481
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
482
|
+
template.push(this.parseViewChild());
|
|
483
|
+
}
|
|
484
|
+
this.expect(TokenType.RBRACE);
|
|
485
|
+
|
|
486
|
+
return new ASTNode(NodeType.EachDirective, { itemName, iterable, template });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Parse event directive
|
|
491
|
+
*/
|
|
492
|
+
parseEventDirective(event) {
|
|
493
|
+
this.expect(TokenType.LPAREN);
|
|
494
|
+
const handler = this.parseExpression();
|
|
495
|
+
this.expect(TokenType.RPAREN);
|
|
496
|
+
|
|
497
|
+
const children = [];
|
|
498
|
+
if (this.is(TokenType.LBRACE)) {
|
|
499
|
+
this.advance();
|
|
500
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
501
|
+
children.push(this.parseViewChild());
|
|
502
|
+
}
|
|
503
|
+
this.expect(TokenType.RBRACE);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return new ASTNode(NodeType.EventDirective, { event, handler, children });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Parse expression (simplified)
|
|
511
|
+
*/
|
|
512
|
+
parseExpression() {
|
|
513
|
+
return this.parseOrExpression();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Parse OR expression
|
|
518
|
+
*/
|
|
519
|
+
parseOrExpression() {
|
|
520
|
+
let left = this.parseAndExpression();
|
|
521
|
+
|
|
522
|
+
while (this.is(TokenType.OR)) {
|
|
523
|
+
this.advance();
|
|
524
|
+
const right = this.parseAndExpression();
|
|
525
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator: '||', left, right });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return left;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Parse AND expression
|
|
533
|
+
*/
|
|
534
|
+
parseAndExpression() {
|
|
535
|
+
let left = this.parseComparisonExpression();
|
|
536
|
+
|
|
537
|
+
while (this.is(TokenType.AND)) {
|
|
538
|
+
this.advance();
|
|
539
|
+
const right = this.parseComparisonExpression();
|
|
540
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator: '&&', left, right });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return left;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Parse comparison expression
|
|
548
|
+
*/
|
|
549
|
+
parseComparisonExpression() {
|
|
550
|
+
let left = this.parseAdditiveExpression();
|
|
551
|
+
|
|
552
|
+
while (this.isAny(TokenType.EQEQ, TokenType.NEQ, TokenType.LT, TokenType.GT,
|
|
553
|
+
TokenType.LTE, TokenType.GTE)) {
|
|
554
|
+
const operator = this.advance().value;
|
|
555
|
+
const right = this.parseAdditiveExpression();
|
|
556
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return left;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Parse additive expression
|
|
564
|
+
*/
|
|
565
|
+
parseAdditiveExpression() {
|
|
566
|
+
let left = this.parseMultiplicativeExpression();
|
|
567
|
+
|
|
568
|
+
while (this.isAny(TokenType.PLUS, TokenType.MINUS)) {
|
|
569
|
+
const operator = this.advance().value;
|
|
570
|
+
const right = this.parseMultiplicativeExpression();
|
|
571
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return left;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Parse multiplicative expression
|
|
579
|
+
*/
|
|
580
|
+
parseMultiplicativeExpression() {
|
|
581
|
+
let left = this.parseUnaryExpression();
|
|
582
|
+
|
|
583
|
+
while (this.isAny(TokenType.STAR, TokenType.SLASH)) {
|
|
584
|
+
const operator = this.advance().value;
|
|
585
|
+
const right = this.parseUnaryExpression();
|
|
586
|
+
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return left;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Parse unary expression
|
|
594
|
+
*/
|
|
595
|
+
parseUnaryExpression() {
|
|
596
|
+
if (this.is(TokenType.NOT)) {
|
|
597
|
+
this.advance();
|
|
598
|
+
const argument = this.parseUnaryExpression();
|
|
599
|
+
return new ASTNode(NodeType.UnaryExpression, { operator: '!', argument });
|
|
600
|
+
}
|
|
601
|
+
if (this.is(TokenType.MINUS)) {
|
|
602
|
+
this.advance();
|
|
603
|
+
const argument = this.parseUnaryExpression();
|
|
604
|
+
return new ASTNode(NodeType.UnaryExpression, { operator: '-', argument });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return this.parsePostfixExpression();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Parse postfix expression (++, --)
|
|
612
|
+
*/
|
|
613
|
+
parsePostfixExpression() {
|
|
614
|
+
let expr = this.parsePrimaryExpression();
|
|
615
|
+
|
|
616
|
+
while (this.isAny(TokenType.PLUSPLUS, TokenType.MINUSMINUS)) {
|
|
617
|
+
const operator = this.advance().value;
|
|
618
|
+
expr = new ASTNode(NodeType.UpdateExpression, {
|
|
619
|
+
operator,
|
|
620
|
+
argument: expr,
|
|
621
|
+
prefix: false
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return expr;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Parse primary expression
|
|
630
|
+
*/
|
|
631
|
+
parsePrimaryExpression() {
|
|
632
|
+
if (this.is(TokenType.LPAREN)) {
|
|
633
|
+
this.advance();
|
|
634
|
+
const expr = this.parseExpression();
|
|
635
|
+
this.expect(TokenType.RPAREN);
|
|
636
|
+
return expr;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (this.is(TokenType.NUMBER)) {
|
|
640
|
+
const token = this.advance();
|
|
641
|
+
return new ASTNode(NodeType.Literal, { value: token.value });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (this.is(TokenType.STRING)) {
|
|
645
|
+
const token = this.advance();
|
|
646
|
+
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (this.is(TokenType.TRUE)) {
|
|
650
|
+
this.advance();
|
|
651
|
+
return new ASTNode(NodeType.Literal, { value: true });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (this.is(TokenType.FALSE)) {
|
|
655
|
+
this.advance();
|
|
656
|
+
return new ASTNode(NodeType.Literal, { value: false });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (this.is(TokenType.NULL)) {
|
|
660
|
+
this.advance();
|
|
661
|
+
return new ASTNode(NodeType.Literal, { value: null });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (this.is(TokenType.IDENT)) {
|
|
665
|
+
return this.parseIdentifierOrExpression();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Unexpected token ${this.current()?.type} in expression at line ${this.current()?.line}`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Parse identifier with possible member access and calls
|
|
675
|
+
*/
|
|
676
|
+
parseIdentifierOrExpression() {
|
|
677
|
+
let expr = new ASTNode(NodeType.Identifier, { name: this.advance().value });
|
|
678
|
+
|
|
679
|
+
while (true) {
|
|
680
|
+
if (this.is(TokenType.DOT)) {
|
|
681
|
+
this.advance();
|
|
682
|
+
const property = this.expect(TokenType.IDENT);
|
|
683
|
+
expr = new ASTNode(NodeType.MemberExpression, {
|
|
684
|
+
object: expr,
|
|
685
|
+
property: property.value
|
|
686
|
+
});
|
|
687
|
+
} else if (this.is(TokenType.LBRACKET)) {
|
|
688
|
+
this.advance();
|
|
689
|
+
const property = this.parseExpression();
|
|
690
|
+
this.expect(TokenType.RBRACKET);
|
|
691
|
+
expr = new ASTNode(NodeType.MemberExpression, {
|
|
692
|
+
object: expr,
|
|
693
|
+
property,
|
|
694
|
+
computed: true
|
|
695
|
+
});
|
|
696
|
+
} else if (this.is(TokenType.LPAREN)) {
|
|
697
|
+
this.advance();
|
|
698
|
+
const args = [];
|
|
699
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
700
|
+
args.push(this.parseExpression());
|
|
701
|
+
if (this.is(TokenType.COMMA)) {
|
|
702
|
+
this.advance();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
this.expect(TokenType.RPAREN);
|
|
706
|
+
expr = new ASTNode(NodeType.CallExpression, { callee: expr, arguments: args });
|
|
707
|
+
} else {
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return expr;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Parse actions block
|
|
717
|
+
*/
|
|
718
|
+
parseActionsBlock() {
|
|
719
|
+
this.expect(TokenType.ACTIONS);
|
|
720
|
+
this.expect(TokenType.LBRACE);
|
|
721
|
+
|
|
722
|
+
const functions = [];
|
|
723
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
724
|
+
functions.push(this.parseFunctionDeclaration());
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
this.expect(TokenType.RBRACE);
|
|
728
|
+
return new ASTNode(NodeType.ActionsBlock, { functions });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Parse function declaration
|
|
733
|
+
*/
|
|
734
|
+
parseFunctionDeclaration() {
|
|
735
|
+
let async = false;
|
|
736
|
+
if (this.is(TokenType.IDENT) && this.current().value === 'async') {
|
|
737
|
+
this.advance();
|
|
738
|
+
async = true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
742
|
+
this.expect(TokenType.LPAREN);
|
|
743
|
+
|
|
744
|
+
const params = [];
|
|
745
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
746
|
+
params.push(this.expect(TokenType.IDENT).value);
|
|
747
|
+
if (this.is(TokenType.COMMA)) {
|
|
748
|
+
this.advance();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
this.expect(TokenType.RPAREN);
|
|
752
|
+
|
|
753
|
+
// Parse function body as raw JS
|
|
754
|
+
this.expect(TokenType.LBRACE);
|
|
755
|
+
const body = this.parseFunctionBody();
|
|
756
|
+
this.expect(TokenType.RBRACE);
|
|
757
|
+
|
|
758
|
+
return new ASTNode(NodeType.FunctionDeclaration, { name, params, body, async });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Parse function body (raw content between braces)
|
|
763
|
+
*/
|
|
764
|
+
parseFunctionBody() {
|
|
765
|
+
// Simplified: collect all tokens until matching }
|
|
766
|
+
const statements = [];
|
|
767
|
+
let braceCount = 1;
|
|
768
|
+
|
|
769
|
+
while (!this.is(TokenType.EOF)) {
|
|
770
|
+
if (this.is(TokenType.LBRACE)) {
|
|
771
|
+
braceCount++;
|
|
772
|
+
} else if (this.is(TokenType.RBRACE)) {
|
|
773
|
+
braceCount--;
|
|
774
|
+
if (braceCount === 0) break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Collect raw token for reconstruction
|
|
778
|
+
statements.push(this.current());
|
|
779
|
+
this.advance();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return statements;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Parse style block
|
|
787
|
+
*/
|
|
788
|
+
parseStyleBlock() {
|
|
789
|
+
this.expect(TokenType.STYLE);
|
|
790
|
+
this.expect(TokenType.LBRACE);
|
|
791
|
+
|
|
792
|
+
const rules = [];
|
|
793
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
794
|
+
rules.push(this.parseStyleRule());
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.expect(TokenType.RBRACE);
|
|
798
|
+
return new ASTNode(NodeType.StyleBlock, { rules });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Parse style rule
|
|
803
|
+
*/
|
|
804
|
+
parseStyleRule() {
|
|
805
|
+
// Parse selector
|
|
806
|
+
let selector = '';
|
|
807
|
+
while (!this.is(TokenType.LBRACE) && !this.is(TokenType.EOF)) {
|
|
808
|
+
selector += this.advance().value;
|
|
809
|
+
}
|
|
810
|
+
selector = selector.trim();
|
|
811
|
+
|
|
812
|
+
this.expect(TokenType.LBRACE);
|
|
813
|
+
|
|
814
|
+
const properties = [];
|
|
815
|
+
const nestedRules = [];
|
|
816
|
+
|
|
817
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
818
|
+
// Check if this is a nested rule or a property
|
|
819
|
+
if (this.isNestedRule()) {
|
|
820
|
+
nestedRules.push(this.parseStyleRule());
|
|
821
|
+
} else {
|
|
822
|
+
properties.push(this.parseStyleProperty());
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
this.expect(TokenType.RBRACE);
|
|
827
|
+
return new ASTNode(NodeType.StyleRule, { selector, properties, nestedRules });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Check if current position is a nested rule
|
|
832
|
+
*/
|
|
833
|
+
isNestedRule() {
|
|
834
|
+
// Look ahead to see if there's a { before a : or newline
|
|
835
|
+
let i = 0;
|
|
836
|
+
while (this.peek(i) && this.peek(i).type !== TokenType.EOF) {
|
|
837
|
+
const token = this.peek(i);
|
|
838
|
+
if (token.type === TokenType.LBRACE) return true;
|
|
839
|
+
if (token.type === TokenType.COLON) return false;
|
|
840
|
+
if (token.type === TokenType.RBRACE) return false;
|
|
841
|
+
i++;
|
|
842
|
+
}
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Parse style property
|
|
848
|
+
*/
|
|
849
|
+
parseStyleProperty() {
|
|
850
|
+
let name = '';
|
|
851
|
+
while (!this.is(TokenType.COLON) && !this.is(TokenType.EOF)) {
|
|
852
|
+
name += this.advance().value;
|
|
853
|
+
}
|
|
854
|
+
name = name.trim();
|
|
855
|
+
|
|
856
|
+
this.expect(TokenType.COLON);
|
|
857
|
+
|
|
858
|
+
let value = '';
|
|
859
|
+
while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) &&
|
|
860
|
+
!this.is(TokenType.EOF) && !this.isPropertyStart()) {
|
|
861
|
+
value += this.advance().value + ' ';
|
|
862
|
+
}
|
|
863
|
+
value = value.trim();
|
|
864
|
+
|
|
865
|
+
if (this.is(TokenType.SEMICOLON)) {
|
|
866
|
+
this.advance();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return new ASTNode(NodeType.StyleProperty, { name, value });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Check if current position starts a new property
|
|
874
|
+
*/
|
|
875
|
+
isPropertyStart() {
|
|
876
|
+
// Check if it looks like: identifier followed by :
|
|
877
|
+
if (!this.is(TokenType.IDENT)) return false;
|
|
878
|
+
let i = 1;
|
|
879
|
+
while (this.peek(i) && this.peek(i).type === TokenType.IDENT) {
|
|
880
|
+
i++;
|
|
881
|
+
}
|
|
882
|
+
return this.peek(i)?.type === TokenType.COLON;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Parse source code into AST
|
|
888
|
+
*/
|
|
889
|
+
export function parse(source) {
|
|
890
|
+
const tokens = tokenize(source);
|
|
891
|
+
const parser = new Parser(tokens);
|
|
892
|
+
return parser.parse();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export default {
|
|
896
|
+
NodeType,
|
|
897
|
+
ASTNode,
|
|
898
|
+
Parser,
|
|
899
|
+
parse
|
|
900
|
+
};
|