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.
@@ -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
+ }