tova 0.3.0 → 0.3.2
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/bin/tova.js +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Client-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when client { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import * as AST from './ast.js';
|
|
6
|
+
|
|
7
|
+
export function installClientParser(ParserClass) {
|
|
8
|
+
if (ParserClass.prototype._clientParserInstalled) return;
|
|
9
|
+
ParserClass.prototype._clientParserInstalled = true;
|
|
10
|
+
|
|
11
|
+
ParserClass.prototype.parseClientBlock = function() {
|
|
12
|
+
const l = this.loc();
|
|
13
|
+
this.expect(TokenType.CLIENT);
|
|
14
|
+
// Optional block name: client "admin" { }
|
|
15
|
+
let name = null;
|
|
16
|
+
if (this.check(TokenType.STRING)) {
|
|
17
|
+
name = this.advance().value;
|
|
18
|
+
}
|
|
19
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'client'");
|
|
20
|
+
const body = [];
|
|
21
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
22
|
+
try {
|
|
23
|
+
const stmt = this.parseClientStatement();
|
|
24
|
+
if (stmt) body.push(stmt);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
this.errors.push(e);
|
|
27
|
+
this._synchronizeBlock();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close client block");
|
|
31
|
+
return new AST.ClientBlock(body, l, name);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
ParserClass.prototype.parseClientStatement = function() {
|
|
35
|
+
if (this.check(TokenType.STATE)) return this.parseState();
|
|
36
|
+
if (this.check(TokenType.COMPUTED)) return this.parseComputed();
|
|
37
|
+
if (this.check(TokenType.EFFECT)) return this.parseEffect();
|
|
38
|
+
if (this.check(TokenType.COMPONENT)) return this.parseComponent();
|
|
39
|
+
if (this.check(TokenType.STORE)) return this.parseStore();
|
|
40
|
+
return this.parseStatement();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
ParserClass.prototype.parseStore = function() {
|
|
44
|
+
const l = this.loc();
|
|
45
|
+
this.expect(TokenType.STORE);
|
|
46
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected store name").value;
|
|
47
|
+
this.expect(TokenType.LBRACE, "Expected '{' after store name");
|
|
48
|
+
|
|
49
|
+
const body = [];
|
|
50
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
51
|
+
if (this.check(TokenType.STATE)) {
|
|
52
|
+
body.push(this.parseState());
|
|
53
|
+
} else if (this.check(TokenType.COMPUTED)) {
|
|
54
|
+
body.push(this.parseComputed());
|
|
55
|
+
} else if (this.check(TokenType.FN) && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
56
|
+
body.push(this.parseFunctionDeclaration());
|
|
57
|
+
} else {
|
|
58
|
+
this.error("Expected 'state', 'computed', or 'fn' inside store block");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close store block");
|
|
62
|
+
|
|
63
|
+
return new AST.StoreDeclaration(name, body, l);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
ParserClass.prototype.parseState = function() {
|
|
67
|
+
const l = this.loc();
|
|
68
|
+
this.expect(TokenType.STATE);
|
|
69
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected state variable name").value;
|
|
70
|
+
|
|
71
|
+
let typeAnnotation = null;
|
|
72
|
+
if (this.match(TokenType.COLON)) {
|
|
73
|
+
typeAnnotation = this.parseTypeAnnotation();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.expect(TokenType.ASSIGN, "Expected '=' in state declaration");
|
|
77
|
+
const value = this.parseExpression();
|
|
78
|
+
|
|
79
|
+
return new AST.StateDeclaration(name, typeAnnotation, value, l);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ParserClass.prototype.parseComputed = function() {
|
|
83
|
+
const l = this.loc();
|
|
84
|
+
this.expect(TokenType.COMPUTED);
|
|
85
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected computed variable name").value;
|
|
86
|
+
this.expect(TokenType.ASSIGN, "Expected '=' in computed declaration");
|
|
87
|
+
const expr = this.parseExpression();
|
|
88
|
+
|
|
89
|
+
return new AST.ComputedDeclaration(name, expr, l);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ParserClass.prototype.parseEffect = function() {
|
|
93
|
+
const l = this.loc();
|
|
94
|
+
this.expect(TokenType.EFFECT);
|
|
95
|
+
const body = this.parseBlock();
|
|
96
|
+
return new AST.EffectDeclaration(body, l);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
ParserClass.prototype.parseComponent = function() {
|
|
100
|
+
const l = this.loc();
|
|
101
|
+
this.expect(TokenType.COMPONENT);
|
|
102
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected component name").value;
|
|
103
|
+
|
|
104
|
+
let params = [];
|
|
105
|
+
if (this.match(TokenType.LPAREN)) {
|
|
106
|
+
params = this.parseParameterList();
|
|
107
|
+
this.expect(TokenType.RPAREN, "Expected ')' after component parameters");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.expect(TokenType.LBRACE, "Expected '{' to open component body");
|
|
111
|
+
const body = [];
|
|
112
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
113
|
+
if (this.check(TokenType.STYLE_BLOCK)) {
|
|
114
|
+
const sl = this.loc();
|
|
115
|
+
const css = this.current().value;
|
|
116
|
+
this.advance();
|
|
117
|
+
body.push(new AST.ComponentStyleBlock(css, sl));
|
|
118
|
+
} else if (this.check(TokenType.LESS) && this._looksLikeJSX()) {
|
|
119
|
+
body.push(this.parseJSXElementOrFragment());
|
|
120
|
+
} else if (this.check(TokenType.STATE)) {
|
|
121
|
+
body.push(this.parseState());
|
|
122
|
+
} else if (this.check(TokenType.COMPUTED)) {
|
|
123
|
+
body.push(this.parseComputed());
|
|
124
|
+
} else if (this.check(TokenType.EFFECT)) {
|
|
125
|
+
body.push(this.parseEffect());
|
|
126
|
+
} else if (this.check(TokenType.COMPONENT)) {
|
|
127
|
+
body.push(this.parseComponent());
|
|
128
|
+
} else {
|
|
129
|
+
body.push(this.parseStatement());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close component body");
|
|
133
|
+
|
|
134
|
+
return new AST.ComponentDeclaration(name, params, body, l);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ─── JSX-like parsing ─────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
ParserClass.prototype._collapseJSXWhitespace = function(text) {
|
|
140
|
+
let result = text.replace(/\s+/g, ' ');
|
|
141
|
+
if (result.trim() === '') return '';
|
|
142
|
+
return result.trim();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
ParserClass.prototype.parseJSXElementOrFragment = function() {
|
|
146
|
+
// Check if this is a fragment: <>...</>
|
|
147
|
+
if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.GREATER) {
|
|
148
|
+
return this.parseJSXFragment();
|
|
149
|
+
}
|
|
150
|
+
return this.parseJSXElement();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ParserClass.prototype.parseJSXFragment = function() {
|
|
154
|
+
const l = this.loc();
|
|
155
|
+
this.expect(TokenType.LESS, "Expected '<'");
|
|
156
|
+
this.expect(TokenType.GREATER, "Expected '>' in fragment opening");
|
|
157
|
+
|
|
158
|
+
// Parse children until </>
|
|
159
|
+
const children = this.parseJSXFragmentChildren();
|
|
160
|
+
|
|
161
|
+
return new AST.JSXFragment(children, l);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
ParserClass.prototype.parseJSXFragmentChildren = function() {
|
|
165
|
+
const children = [];
|
|
166
|
+
|
|
167
|
+
while (!this.isAtEnd()) {
|
|
168
|
+
// Closing fragment: </>
|
|
169
|
+
if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.SLASH) {
|
|
170
|
+
// Check for </> (fragment close) vs </tag> (error)
|
|
171
|
+
if (this.peek(2).type === TokenType.GREATER) {
|
|
172
|
+
this.advance(); // <
|
|
173
|
+
this.advance(); // /
|
|
174
|
+
this.advance(); // >
|
|
175
|
+
break;
|
|
176
|
+
} else {
|
|
177
|
+
this.error("Unexpected closing tag inside fragment. Use </> to close a fragment");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Nested element or fragment
|
|
182
|
+
if (this.check(TokenType.LESS)) {
|
|
183
|
+
if (this.peek(1).type === TokenType.GREATER) {
|
|
184
|
+
children.push(this.parseJSXFragment());
|
|
185
|
+
} else {
|
|
186
|
+
children.push(this.parseJSXElement());
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// String literal as text
|
|
192
|
+
if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
193
|
+
const str = this.parseStringLiteral();
|
|
194
|
+
children.push(new AST.JSXText(str, this.loc()));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Unquoted JSX text
|
|
199
|
+
if (this.check(TokenType.JSX_TEXT)) {
|
|
200
|
+
const tok = this.advance();
|
|
201
|
+
const text = this._collapseJSXWhitespace(tok.value);
|
|
202
|
+
if (text.length > 0) {
|
|
203
|
+
children.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Expression in braces: {expr}
|
|
209
|
+
if (this.check(TokenType.LBRACE)) {
|
|
210
|
+
this.advance();
|
|
211
|
+
const expr = this.parseExpression();
|
|
212
|
+
this.expect(TokenType.RBRACE, "Expected '}' after JSX expression");
|
|
213
|
+
children.push(new AST.JSXExpression(expr, this.loc()));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// for loop inside JSX
|
|
218
|
+
if (this.check(TokenType.FOR)) {
|
|
219
|
+
children.push(this.parseJSXFor());
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// if inside JSX
|
|
224
|
+
if (this.check(TokenType.IF)) {
|
|
225
|
+
children.push(this.parseJSXIf());
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return children;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
ParserClass.prototype.parseJSXElement = function() {
|
|
236
|
+
const l = this.loc();
|
|
237
|
+
this.expect(TokenType.LESS, "Expected '<'");
|
|
238
|
+
|
|
239
|
+
const tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
|
|
240
|
+
|
|
241
|
+
// Parse attributes (including spread: {...expr})
|
|
242
|
+
const attributes = [];
|
|
243
|
+
while (!this.check(TokenType.GREATER) && !this.check(TokenType.SLASH) && !this.isAtEnd()) {
|
|
244
|
+
// Check for spread attribute: {...expr}
|
|
245
|
+
if (this.check(TokenType.LBRACE) && this.peek(1).type === TokenType.SPREAD) {
|
|
246
|
+
const sl = this.loc();
|
|
247
|
+
this.advance(); // {
|
|
248
|
+
this.advance(); // ...
|
|
249
|
+
const expr = this.parseExpression();
|
|
250
|
+
this.expect(TokenType.RBRACE, "Expected '}' after spread expression");
|
|
251
|
+
attributes.push(new AST.JSXSpreadAttribute(expr, sl));
|
|
252
|
+
} else {
|
|
253
|
+
attributes.push(this.parseJSXAttribute());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Self-closing tag: />
|
|
258
|
+
if (this.match(TokenType.SLASH)) {
|
|
259
|
+
this.expect(TokenType.GREATER, "Expected '>' in self-closing tag");
|
|
260
|
+
return new AST.JSXElement(tag, attributes, [], true, l);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.expect(TokenType.GREATER, "Expected '>'");
|
|
264
|
+
|
|
265
|
+
// Parse children
|
|
266
|
+
const children = this.parseJSXChildren(tag);
|
|
267
|
+
|
|
268
|
+
return new AST.JSXElement(tag, attributes, children, false, l);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
ParserClass.prototype.parseJSXAttribute = function() {
|
|
272
|
+
const l = this.loc();
|
|
273
|
+
// Accept keywords as attribute names (type, class, for, etc. are valid HTML attributes)
|
|
274
|
+
let name;
|
|
275
|
+
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE) || this.check(TokenType.FOR) ||
|
|
276
|
+
this.check(TokenType.IN) || this.check(TokenType.AS) || this.check(TokenType.EXPORT) ||
|
|
277
|
+
this.check(TokenType.STATE) || this.check(TokenType.COMPUTED) || this.check(TokenType.ROUTE)) {
|
|
278
|
+
name = this.advance().value;
|
|
279
|
+
} else {
|
|
280
|
+
this.error("Expected attribute name");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle namespaced attributes: on:click, bind:value, class:active
|
|
284
|
+
if (this.match(TokenType.COLON)) {
|
|
285
|
+
let suffix;
|
|
286
|
+
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.IN)) {
|
|
287
|
+
suffix = this.advance().value;
|
|
288
|
+
} else {
|
|
289
|
+
suffix = this.expect(TokenType.IDENTIFIER, "Expected name after ':'").value;
|
|
290
|
+
}
|
|
291
|
+
name = `${name}:${suffix}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!this.match(TokenType.ASSIGN)) {
|
|
295
|
+
// Boolean attribute: <input disabled />
|
|
296
|
+
return new AST.JSXAttribute(name, new AST.BooleanLiteral(true, l), l);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Value can be {expression} or "string"
|
|
300
|
+
if (this.match(TokenType.LBRACE)) {
|
|
301
|
+
const expr = this.parseExpression();
|
|
302
|
+
this.expect(TokenType.RBRACE, "Expected '}' after attribute expression");
|
|
303
|
+
return new AST.JSXAttribute(name, expr, l);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
307
|
+
const val = this.parseStringLiteral();
|
|
308
|
+
return new AST.JSXAttribute(name, val, l);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.error("Expected attribute value");
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
ParserClass.prototype.parseJSXChildren = function(parentTag) {
|
|
315
|
+
const children = [];
|
|
316
|
+
|
|
317
|
+
while (!this.isAtEnd()) {
|
|
318
|
+
// Closing tag: </tag>
|
|
319
|
+
if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.SLASH) {
|
|
320
|
+
this.advance(); // <
|
|
321
|
+
this.advance(); // /
|
|
322
|
+
const closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
|
|
323
|
+
if (closeTag !== parentTag) {
|
|
324
|
+
this.error(`Mismatched closing tag: expected </${parentTag}>, got </${closeTag}>`);
|
|
325
|
+
}
|
|
326
|
+
this.expect(TokenType.GREATER, "Expected '>' in closing tag");
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Nested element or fragment
|
|
331
|
+
if (this.check(TokenType.LESS)) {
|
|
332
|
+
children.push(this.parseJSXElementOrFragment());
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// String literal as text
|
|
337
|
+
if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
338
|
+
const str = this.parseStringLiteral();
|
|
339
|
+
children.push(new AST.JSXText(str, this.loc()));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Unquoted JSX text
|
|
344
|
+
if (this.check(TokenType.JSX_TEXT)) {
|
|
345
|
+
const tok = this.advance();
|
|
346
|
+
const text = this._collapseJSXWhitespace(tok.value);
|
|
347
|
+
if (text.length > 0) {
|
|
348
|
+
children.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Expression in braces: {expr}
|
|
354
|
+
if (this.check(TokenType.LBRACE)) {
|
|
355
|
+
this.advance();
|
|
356
|
+
const expr = this.parseExpression();
|
|
357
|
+
this.expect(TokenType.RBRACE, "Expected '}' after JSX expression");
|
|
358
|
+
children.push(new AST.JSXExpression(expr, this.loc()));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// for loop inside JSX
|
|
363
|
+
if (this.check(TokenType.FOR)) {
|
|
364
|
+
children.push(this.parseJSXFor());
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// if inside JSX
|
|
369
|
+
if (this.check(TokenType.IF)) {
|
|
370
|
+
children.push(this.parseJSXIf());
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return children;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
ParserClass.prototype.parseJSXFor = function() {
|
|
381
|
+
const l = this.loc();
|
|
382
|
+
this.expect(TokenType.FOR);
|
|
383
|
+
|
|
384
|
+
// Support destructuring: for [i, item] in ..., for {name, age} in ...
|
|
385
|
+
let variable;
|
|
386
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
387
|
+
// Array destructuring: [a, b]
|
|
388
|
+
this.advance(); // consume [
|
|
389
|
+
const elements = [];
|
|
390
|
+
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
391
|
+
elements.push(this.expect(TokenType.IDENTIFIER, "Expected variable name in array pattern").value);
|
|
392
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
393
|
+
}
|
|
394
|
+
this.expect(TokenType.RBRACKET, "Expected ']' in destructuring pattern");
|
|
395
|
+
variable = `[${elements.join(', ')}]`;
|
|
396
|
+
} else if (this.check(TokenType.LBRACE)) {
|
|
397
|
+
// Object destructuring: {name, age}
|
|
398
|
+
this.advance(); // consume {
|
|
399
|
+
const props = [];
|
|
400
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
401
|
+
props.push(this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value);
|
|
402
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
403
|
+
}
|
|
404
|
+
this.expect(TokenType.RBRACE, "Expected '}' in destructuring pattern");
|
|
405
|
+
variable = `{${props.join(', ')}}`;
|
|
406
|
+
} else {
|
|
407
|
+
variable = this.expect(TokenType.IDENTIFIER, "Expected loop variable").value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.expect(TokenType.IN, "Expected 'in' in for loop");
|
|
411
|
+
const iterable = this.parseExpression();
|
|
412
|
+
|
|
413
|
+
// Optional key expression: for item in items key={item.id} { ... }
|
|
414
|
+
let keyExpr = null;
|
|
415
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'key') {
|
|
416
|
+
this.advance(); // consume 'key'
|
|
417
|
+
this.expect(TokenType.ASSIGN, "Expected '=' after 'key'");
|
|
418
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'key='");
|
|
419
|
+
keyExpr = this.parseExpression();
|
|
420
|
+
this.expect(TokenType.RBRACE, "Expected '}' after key expression");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.expect(TokenType.LBRACE, "Expected '{' in JSX for body");
|
|
424
|
+
|
|
425
|
+
const body = [];
|
|
426
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
427
|
+
if (this.check(TokenType.LESS)) {
|
|
428
|
+
body.push(this.parseJSXElementOrFragment());
|
|
429
|
+
} else if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
430
|
+
body.push(new AST.JSXText(this.parseStringLiteral(), this.loc()));
|
|
431
|
+
} else if (this.check(TokenType.JSX_TEXT)) {
|
|
432
|
+
const tok = this.advance();
|
|
433
|
+
const text = this._collapseJSXWhitespace(tok.value);
|
|
434
|
+
if (text.length > 0) {
|
|
435
|
+
body.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
436
|
+
}
|
|
437
|
+
} else if (this.check(TokenType.LBRACE)) {
|
|
438
|
+
this.advance();
|
|
439
|
+
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
440
|
+
this.expect(TokenType.RBRACE);
|
|
441
|
+
} else {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close JSX for body");
|
|
446
|
+
|
|
447
|
+
return new AST.JSXFor(variable, iterable, body, l, keyExpr);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
ParserClass.prototype._parseJSXIfBody = function() {
|
|
451
|
+
const body = [];
|
|
452
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
453
|
+
if (this.check(TokenType.LESS)) {
|
|
454
|
+
body.push(this.parseJSXElementOrFragment());
|
|
455
|
+
} else if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
456
|
+
body.push(new AST.JSXText(this.parseStringLiteral(), this.loc()));
|
|
457
|
+
} else if (this.check(TokenType.JSX_TEXT)) {
|
|
458
|
+
const tok = this.advance();
|
|
459
|
+
const text = this._collapseJSXWhitespace(tok.value);
|
|
460
|
+
if (text.length > 0) {
|
|
461
|
+
body.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
462
|
+
}
|
|
463
|
+
} else if (this.check(TokenType.LBRACE)) {
|
|
464
|
+
this.advance();
|
|
465
|
+
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
466
|
+
this.expect(TokenType.RBRACE);
|
|
467
|
+
} else {
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return body;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
ParserClass.prototype.parseJSXIf = function() {
|
|
475
|
+
const l = this.loc();
|
|
476
|
+
this.expect(TokenType.IF);
|
|
477
|
+
const condition = this.parseExpression();
|
|
478
|
+
this.expect(TokenType.LBRACE, "Expected '{' in JSX if body");
|
|
479
|
+
const consequent = this._parseJSXIfBody();
|
|
480
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close JSX if body");
|
|
481
|
+
|
|
482
|
+
// Parse elif chains
|
|
483
|
+
const alternates = [];
|
|
484
|
+
while (this.check(TokenType.ELIF)) {
|
|
485
|
+
this.advance(); // consume 'elif'
|
|
486
|
+
const elifCond = this.parseExpression();
|
|
487
|
+
this.expect(TokenType.LBRACE, "Expected '{' in JSX elif body");
|
|
488
|
+
const elifBody = this._parseJSXIfBody();
|
|
489
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close JSX elif body");
|
|
490
|
+
alternates.push({ condition: elifCond, body: elifBody });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Parse optional else
|
|
494
|
+
let alternate = null;
|
|
495
|
+
if (this.check(TokenType.ELSE)) {
|
|
496
|
+
this.advance();
|
|
497
|
+
this.expect(TokenType.LBRACE);
|
|
498
|
+
alternate = this._parseJSXIfBody();
|
|
499
|
+
this.expect(TokenType.RBRACE);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return new AST.JSXIf(condition, consequent, alternate, l, alternates);
|
|
503
|
+
};
|
|
504
|
+
}
|