@jetstart/core 1.7.0 → 2.0.1

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.
Files changed (40) hide show
  1. package/README.md +189 -74
  2. package/dist/build/dex-generator.d.ts +27 -0
  3. package/dist/build/dex-generator.js +202 -0
  4. package/dist/build/dsl-parser.d.ts +3 -30
  5. package/dist/build/dsl-parser.js +67 -240
  6. package/dist/build/dsl-types.d.ts +8 -0
  7. package/dist/build/gradle.d.ts +51 -0
  8. package/dist/build/gradle.js +233 -1
  9. package/dist/build/hot-reload-service.d.ts +36 -0
  10. package/dist/build/hot-reload-service.js +179 -0
  11. package/dist/build/js-compiler-service.d.ts +61 -0
  12. package/dist/build/js-compiler-service.js +421 -0
  13. package/dist/build/kotlin-compiler.d.ts +54 -0
  14. package/dist/build/kotlin-compiler.js +450 -0
  15. package/dist/build/kotlin-parser.d.ts +91 -0
  16. package/dist/build/kotlin-parser.js +1030 -0
  17. package/dist/build/override-generator.d.ts +54 -0
  18. package/dist/build/override-generator.js +430 -0
  19. package/dist/server/index.d.ts +16 -1
  20. package/dist/server/index.js +147 -42
  21. package/dist/websocket/handler.d.ts +20 -4
  22. package/dist/websocket/handler.js +73 -38
  23. package/dist/websocket/index.d.ts +8 -0
  24. package/dist/websocket/index.js +15 -11
  25. package/dist/websocket/manager.d.ts +2 -2
  26. package/dist/websocket/manager.js +1 -1
  27. package/package.json +3 -3
  28. package/src/build/dex-generator.ts +197 -0
  29. package/src/build/dsl-parser.ts +73 -272
  30. package/src/build/dsl-types.ts +9 -0
  31. package/src/build/gradle.ts +259 -1
  32. package/src/build/hot-reload-service.ts +178 -0
  33. package/src/build/js-compiler-service.ts +411 -0
  34. package/src/build/kotlin-compiler.ts +460 -0
  35. package/src/build/kotlin-parser.ts +1043 -0
  36. package/src/build/override-generator.ts +478 -0
  37. package/src/server/index.ts +162 -54
  38. package/src/websocket/handler.ts +94 -56
  39. package/src/websocket/index.ts +27 -14
  40. package/src/websocket/manager.ts +2 -2
@@ -0,0 +1,1043 @@
1
+ // import { log } from '../utils/logger'; // Use local for debug
2
+
3
+ // Simple logger
4
+ const log = (msg: string) => console.log(`[KotlinParser] ${msg}`);
5
+
6
+ export enum TokenType {
7
+ Identifier,
8
+ StringLiteral,
9
+ NumberLiteral,
10
+ ParenOpen,
11
+ ParenClose,
12
+ BraceOpen,
13
+ BraceClose,
14
+ Equals,
15
+ Comma,
16
+ Dot,
17
+ Colon,
18
+ Keyword,
19
+ Operator,
20
+ EOF
21
+ }
22
+
23
+ export interface Token {
24
+ type: TokenType;
25
+ value: string;
26
+ line: number;
27
+ column: number;
28
+ }
29
+
30
+ export class Tokenizer {
31
+ private content: string;
32
+ private position: number = 0;
33
+ private line: number = 1;
34
+ private column: number = 1;
35
+
36
+ constructor(content: string) {
37
+ this.content = content;
38
+ }
39
+
40
+ tokenize(): Token[] {
41
+ const tokens: Token[] = [];
42
+ while (this.position < this.content.length) {
43
+ const char = this.content[this.position];
44
+
45
+ if (/\s/.test(char)) {
46
+ this.advance();
47
+ continue;
48
+ }
49
+
50
+ if (char === '/' && this.peek() === '/') {
51
+ this.skipLineComment();
52
+ continue;
53
+ }
54
+
55
+ if (char === '/' && this.peek() === '*') {
56
+ this.skipBlockComment();
57
+ continue;
58
+ }
59
+
60
+ if (/[a-zA-Z_]/.test(char)) {
61
+ tokens.push(this.readIdentifier());
62
+ continue;
63
+ }
64
+
65
+ if (/[0-9]/.test(char)) {
66
+ tokens.push(this.readNumber());
67
+ continue;
68
+ }
69
+
70
+ if (char === '"') {
71
+ tokens.push(this.readString());
72
+ continue;
73
+ }
74
+
75
+ // Symbols
76
+ if (char === '(') tokens.push(this.createToken(TokenType.ParenOpen, '('));
77
+ else if (char === ')') tokens.push(this.createToken(TokenType.ParenClose, ')'));
78
+ else if (char === '{') tokens.push(this.createToken(TokenType.BraceOpen, '{'));
79
+ else if (char === '}') tokens.push(this.createToken(TokenType.BraceClose, '}'));
80
+ else if (char === '=') tokens.push(this.createToken(TokenType.Equals, '='));
81
+ else if (char === ',') tokens.push(this.createToken(TokenType.Comma, ','));
82
+ else if (char === '.') tokens.push(this.createToken(TokenType.Dot, '.'));
83
+ else if (char === ':' && this.peek() === ':') {
84
+ // Method reference operator ::
85
+ tokens.push(this.createToken(TokenType.Operator, '::'));
86
+ this.advance(); // consume first :
87
+ }
88
+ else if (char === ':') tokens.push(this.createToken(TokenType.Colon, ':'));
89
+ else tokens.push(this.createToken(TokenType.Operator, char)); // Generic operator for others
90
+
91
+ this.advance();
92
+ }
93
+ tokens.push({ type: TokenType.EOF, value: '', line: this.line, column: this.column });
94
+ return tokens;
95
+ }
96
+
97
+ private advance() {
98
+ if (this.content[this.position] === '\n') {
99
+ this.line++;
100
+ this.column = 1;
101
+ } else {
102
+ this.column++;
103
+ }
104
+ this.position++;
105
+ }
106
+
107
+ private peek(): string {
108
+ return this.position + 1 < this.content.length ? this.content[this.position + 1] : '';
109
+ }
110
+
111
+ private createToken(type: TokenType, value: string): Token {
112
+ return { type, value, line: this.line, column: this.column };
113
+ }
114
+
115
+ private skipLineComment() {
116
+ while (this.position < this.content.length && this.content[this.position] !== '\n') {
117
+ this.position++; // Don't advance line count here, standard advance handles \n char
118
+ }
119
+ // Let the main loop handle the newline char
120
+ }
121
+
122
+ private skipBlockComment() {
123
+ this.position += 2; // Skip /*
124
+ while (this.position < this.content.length) {
125
+ if (this.content[this.position] === '*' && this.peek() === '/') {
126
+ this.position += 2;
127
+ return;
128
+ }
129
+ this.advance();
130
+ }
131
+ }
132
+
133
+ private readIdentifier(): Token {
134
+ const startLine = this.line;
135
+ const startColumn = this.column;
136
+ let value = '';
137
+ while (this.position < this.content.length && /[a-zA-Z0-9_]/.test(this.content[this.position])) {
138
+ value += this.content[this.position];
139
+ this.advance(); // Don't use standard advance for loop safety? Actually standard advance is safe
140
+ }
141
+
142
+ // Check keywords
143
+ const keywords = ['val', 'var', 'fun', 'if', 'else', 'true', 'false', 'null', 'return', 'package', 'import', 'class', 'object'];
144
+ const type = keywords.includes(value) ? TokenType.Keyword : TokenType.Identifier;
145
+
146
+ // Correction: We advanced tokens in the loop, but we need to create token with START position
147
+ return { type, value, line: startLine, column: startColumn };
148
+ }
149
+
150
+ private readNumber(): Token {
151
+ const startLine = this.line;
152
+ const startColumn = this.column;
153
+ let value = '';
154
+ while (this.position < this.content.length && /[0-9]/.test(this.content[this.position])) {
155
+ value += this.content[this.position];
156
+ this.advance(); // Safe to advance as we checked condition
157
+ }
158
+ // Don't consume dot here to support 16.dp as 16, ., dp
159
+ return { type: TokenType.NumberLiteral, value, line: startLine, column: startColumn };
160
+ }
161
+
162
+ private readString(): Token {
163
+ const startLine = this.line;
164
+ const startColumn = this.column;
165
+ this.advance(); // Skip open quote
166
+ let value = '';
167
+ while (this.position < this.content.length && this.content[this.position] !== '"') {
168
+ if (this.content[this.position] === '\\') {
169
+ this.advance();
170
+ if (this.position < this.content.length) value += this.content[this.position];
171
+ } else {
172
+ value += this.content[this.position];
173
+ }
174
+ this.advance();
175
+ }
176
+ this.advance(); // Skip close quote
177
+ return { type: TokenType.StringLiteral, value, line: startLine, column: startColumn };
178
+ }
179
+ }
180
+
181
+ import { DSLElement } from './dsl-types';
182
+
183
+ export class KotlinParser {
184
+ private tokens: Token[];
185
+ private position: number = 0;
186
+ private library: Map<string, string>;
187
+
188
+ constructor(tokens: Token[], library: Map<string, string> = new Map()) {
189
+ this.tokens = tokens;
190
+ this.library = library;
191
+ }
192
+
193
+ parse(): any {
194
+ log(`Parsing with ${this.tokens.length} tokens`);
195
+ const elements = this.parseBlockContent();
196
+
197
+ // Create logic root (like "Column")
198
+ let root: any;
199
+ if (elements.length === 1) {
200
+ root = elements[0];
201
+ } else {
202
+ root = {
203
+ type: 'Column',
204
+ children: elements
205
+ };
206
+ }
207
+
208
+ // Transform to DivKit
209
+ const divBody = this.mapToDivKit(root);
210
+
211
+ // Wrap in standard DivKit structure
212
+ return {
213
+ "templates": {},
214
+ "card": {
215
+ "log_id": "hot_reload_screen",
216
+ "states": [
217
+ {
218
+ "state_id": 0,
219
+ "div": divBody
220
+ }
221
+ ]
222
+ }
223
+ };
224
+ }
225
+
226
+ private mapToDivKit(element: any): any {
227
+ if (!element) return null;
228
+
229
+ // Base properties
230
+ const paddings = this.mapModifiersToPaddings(element.modifier);
231
+ const width = this.mapModifiersToSize(element.modifier, 'width');
232
+ const height = this.mapModifiersToSize(element.modifier, 'height');
233
+
234
+ const commonProps: any = {};
235
+ if (paddings) commonProps.paddings = paddings;
236
+ if (width) commonProps.width = width;
237
+ if (height) commonProps.height = height;
238
+
239
+ // Type mapping
240
+ switch (element.type) {
241
+ case 'Column':
242
+ return {
243
+ type: 'container',
244
+ orientation: 'vertical',
245
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter((Boolean as any)) || [],
246
+ ...commonProps
247
+ };
248
+ case 'Row':
249
+ return {
250
+ type: 'container',
251
+ orientation: 'horizontal',
252
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter((Boolean as any)) || [],
253
+ ...commonProps
254
+ };
255
+ case 'Box':
256
+ return {
257
+ type: 'container',
258
+ layout_mode: 'overlap', // DivKit overlap container
259
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter((Boolean as any)) || [],
260
+ ...commonProps
261
+ };
262
+ case 'Text':
263
+ return {
264
+ type: 'text',
265
+ text: element.text || '',
266
+ text_color: element.color || '#000000',
267
+ font_size: this.mapStyleToSize(element.style),
268
+ ...commonProps
269
+ };
270
+ case 'Button':
271
+ case 'FloatingActionButton':
272
+ // Buttons are containers with actions
273
+ const btnContent = element.children && element.children.length > 0
274
+ ? this.mapToDivKit(element.children[0]) // Icon or Text
275
+ : { type: 'text', text: element.text || "Button" };
276
+
277
+ return {
278
+ type: 'container',
279
+ items: [btnContent],
280
+ actions: element.onClick ? [{
281
+ log_id: "click",
282
+ url: "div-action://set_variable?name=debug_click&value=true" // Placeholder
283
+ // Real logic: url: `div-action://${element.onClick}` if we had logic engine
284
+ }] : [],
285
+ background: [{ type: 'solid', color: element.color || '#6200EE' }],
286
+ ...commonProps
287
+ };
288
+ case 'Icon':
289
+ return {
290
+ type: 'image',
291
+ // DivKit needs URL. We map Icons.Default.Add to a resource or web URL?
292
+ // DivKit supports local resources "android.resource://"
293
+ image_url: "https://img.icons8.com/material-outlined/24/000000/add.png", // Fallback for demo
294
+ tint_color: element.tint,
295
+ width: { type: 'fixed', value: 24 },
296
+ height: { type: 'fixed', value: 24 },
297
+ ...commonProps
298
+ };
299
+ case 'Scaffold':
300
+ // Scaffold is a layout container - map children to DivKit container
301
+ return {
302
+ type: 'container',
303
+ orientation: 'vertical',
304
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter(Boolean) || [],
305
+ ...commonProps
306
+ };
307
+ case 'Card':
308
+ return {
309
+ type: 'container',
310
+ orientation: 'vertical',
311
+ background: [{ type: 'solid', color: '#FFFFFF' }],
312
+ border: { corner_radius: 8 },
313
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter(Boolean) || [],
314
+ ...commonProps
315
+ };
316
+ case 'LazyVerticalStaggeredGrid':
317
+ case 'LazyColumn':
318
+ case 'LazyRow':
319
+ // Lazy lists - map as containers
320
+ return {
321
+ type: 'container',
322
+ orientation: element.type === 'LazyRow' ? 'horizontal' : 'vertical',
323
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter(Boolean) || [],
324
+ ...commonProps
325
+ };
326
+ case 'OutlinedTextField':
327
+ case 'TextField':
328
+ return {
329
+ type: 'input',
330
+ hint_text: element.placeholder || 'Enter text...',
331
+ ...commonProps
332
+ };
333
+ case 'Spacer':
334
+ return {
335
+ type: 'separator',
336
+ delimiter_style: { color: '#00000000' }, // transparent
337
+ ...commonProps
338
+ };
339
+ case 'AlertDialog':
340
+ case 'Dialog':
341
+ // Dialogs - just render the content for preview
342
+ return {
343
+ type: 'container',
344
+ orientation: 'vertical',
345
+ items: element.children?.map((c: any) => this.mapToDivKit(c)).filter(Boolean) || [],
346
+ ...commonProps
347
+ };
348
+ case 'AssistChip':
349
+ case 'SuggestionChip':
350
+ case 'FilterChip':
351
+ return {
352
+ type: 'text',
353
+ text: element.text || 'Chip',
354
+ text_color: '#6200EE',
355
+ font_size: 12,
356
+ border: { corner_radius: 16 },
357
+ background: [{ type: 'solid', color: '#E8DEF8' }],
358
+ paddings: { left: 12, right: 12, top: 6, bottom: 6 },
359
+ ...commonProps
360
+ };
361
+ default:
362
+ // Fallback
363
+ return {
364
+ type: 'text',
365
+ text: `[${element.type}]`,
366
+ text_color: '#FF0000'
367
+ };
368
+ }
369
+ }
370
+
371
+ private mapModifiersToPaddings(modifier: any): any {
372
+ if (!modifier) return null;
373
+ const p: any = {};
374
+ if (modifier.padding) { p.left=modifier.padding; p.right=modifier.padding; p.top=modifier.padding; p.bottom=modifier.padding; }
375
+ if (modifier.paddingHorizontal) { p.left=modifier.paddingHorizontal; p.right=modifier.paddingHorizontal; }
376
+ if (modifier.paddingVertical) { p.top=modifier.paddingVertical; p.bottom=modifier.paddingVertical; }
377
+ return Object.keys(p).length > 0 ? p : null;
378
+ }
379
+
380
+ private mapModifiersToSize(modifier: any, dim: 'width' | 'height'): any {
381
+ if (!modifier) return { type: 'wrap_content' };
382
+ if (dim === 'width' && modifier.fillMaxWidth) return { type: 'match_parent' };
383
+ if (dim === 'height' && modifier.fillMaxHeight) return { type: 'match_parent' };
384
+ if (modifier[dim]) return { type: 'fixed', value: modifier[dim] };
385
+ return { type: 'wrap_content' };
386
+ }
387
+
388
+ private mapStyleToSize(style: string | undefined): number {
389
+ if (!style) return 14;
390
+ if (style.includes('Large')) return 22;
391
+ if (style.includes('Medium')) return 18;
392
+ if (style.includes('Small')) return 14;
393
+ return 14;
394
+ }
395
+
396
+ private parseBlockContent(): DSLElement[] {
397
+ const elements: DSLElement[] = [];
398
+
399
+ // Check for lambda parameters at the start of block: { padding -> ... }
400
+ this.skipLambdaParameters();
401
+
402
+ while (!this.isAtEnd() && !this.check(TokenType.BraceClose)) {
403
+ const startPos = this.position;
404
+ const currentToken = this.peekCurrent();
405
+ log(`[parseBlockContent] pos=${startPos}, token=${currentToken.value} (${TokenType[currentToken.type]})`);
406
+
407
+ try {
408
+ const element = this.parseStatement();
409
+ if (element) {
410
+ log(`[parseBlockContent] Got element: ${element.type}`);
411
+ elements.push(element);
412
+ }
413
+ // If we didn't move forward, force advance to avoid infinite loop
414
+ if (this.position === startPos) {
415
+ log(`[parseBlockContent] Skipping stuck token: ${JSON.stringify(currentToken)}`);
416
+ this.advance();
417
+ }
418
+ } catch (e) {
419
+ log(`Error parsing statement at ${this.position}: ${e}`);
420
+ this.advance(); // Force advance to break loop if stuck
421
+ }
422
+ }
423
+
424
+ log(`[parseBlockContent] Returning ${elements.length} elements`);
425
+ return elements;
426
+ }
427
+
428
+ private parseStatement(): DSLElement | null {
429
+ try {
430
+ if (this.check(TokenType.Identifier)) {
431
+ const nextToken = this.peek();
432
+
433
+ // Case 1: Direct function call - Identifier followed by ( or {
434
+ if (nextToken.type === TokenType.ParenOpen || nextToken.type === TokenType.BraceOpen) {
435
+ this.advance(); // consume identifier so parseFunctionCall can see it as previous()
436
+ return this.parseFunctionCall();
437
+ }
438
+
439
+ // Case 2: Assignment statement - identifier = expression
440
+ // This handles: showAddDialog = true, x = foo.bar(), etc.
441
+ if (nextToken.type === TokenType.Equals) {
442
+ this.advance(); // consume identifier
443
+ this.skipAssignment(); // consume = and expression
444
+ return null; // Assignment is not a UI element
445
+ }
446
+
447
+ // Case 3: Identifier chain (a.b.c) possibly followed by call or assignment
448
+ this.advance(); // consume first identifier
449
+ while (this.match(TokenType.Dot)) {
450
+ this.match(TokenType.Identifier);
451
+ }
452
+
453
+ // After consuming chain, check what follows
454
+ if (this.check(TokenType.Equals)) {
455
+ // Chain assignment: foo.bar = value
456
+ this.skipAssignment();
457
+ return null;
458
+ }
459
+
460
+ if (this.check(TokenType.ParenOpen) || this.check(TokenType.BraceOpen)) {
461
+ // Chain ends with function call
462
+ if (this.match(TokenType.ParenOpen)) {
463
+ this.consumeParenthesesContent(); // consume args (we're already past the open paren)
464
+ }
465
+ if (this.check(TokenType.BraceOpen)) {
466
+ this.consume(TokenType.BraceOpen, "Expect '{'");
467
+ const children = this.parseBlockContent();
468
+ this.consume(TokenType.BraceClose, "Expect '}'");
469
+ return {
470
+ type: 'BlockWrapper',
471
+ children
472
+ };
473
+ }
474
+ return null;
475
+ }
476
+
477
+ return null; // Just an identifier access, skip
478
+ }
479
+
480
+ if (this.match(TokenType.Keyword)) {
481
+ const keyword = this.previous().value;
482
+ if (keyword === 'val' || keyword === 'var') {
483
+ this.skipDeclaration();
484
+ return null;
485
+ }
486
+ if (keyword === 'if') {
487
+ this.consumeUntil(TokenType.BraceOpen); // Skip condition
488
+ if (this.match(TokenType.BraceOpen)) {
489
+ const children = this.parseBlockContent();
490
+ this.consume(TokenType.BraceClose, "Expect '}' after if block");
491
+ return {
492
+ type: 'Box',
493
+ children
494
+ };
495
+ }
496
+ return null;
497
+ }
498
+ }
499
+
500
+ return null;
501
+ } catch (e) {
502
+ log(`CRIT: Error in parseStatement at ${this.position}: ${e}`);
503
+ return null; // Recover gracefully
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Consume parentheses content (after opening paren has been consumed)
509
+ */
510
+ private consumeParenthesesContent() {
511
+ let depth = 1;
512
+ while (depth > 0 && !this.isAtEnd()) {
513
+ if (this.check(TokenType.ParenOpen)) depth++;
514
+ if (this.check(TokenType.ParenClose)) depth--;
515
+ this.advance();
516
+ }
517
+ }
518
+
519
+
520
+ private parseFunctionCall(): DSLElement | null {
521
+ const name = this.previous().value;
522
+ const element: DSLElement = {
523
+ type: name,
524
+ children: []
525
+ };
526
+
527
+ // INLINING LOGIC
528
+ if (this.library && this.library.has(name)) {
529
+ // Consume arguments at call site to advance cursor
530
+ this.consumeParentheses();
531
+
532
+ // Consume optional trailing lambda at call site if present?
533
+ // NoteItem(...) { } - usually not if it's a leaf, but if it has content...
534
+ // For now assuming simplistic NoteItem(...)
535
+
536
+ const body = this.library.get(name)!;
537
+ const subLibrary = new Map(this.library);
538
+ subLibrary.delete(name); // Prevent direct recursion
539
+
540
+ const tokenizer = new Tokenizer(body);
541
+ const subTokens = tokenizer.tokenize();
542
+ const subParser = new KotlinParser(subTokens, subLibrary); // Pass library down
543
+
544
+ // We need to parse the *content* of the function.
545
+ // Function body: "Card { ... }"
546
+ // subParser.parse() will return the Card.
547
+ const inlined = subParser.parse();
548
+ return inlined;
549
+ }
550
+
551
+ // Standard logic: Check for arguments (...)
552
+ let args: any = {};
553
+ if (this.match(TokenType.ParenOpen)) {
554
+ args = this.parseArguments();
555
+ }
556
+
557
+ if (args.modifier) element.modifier = args.modifier;
558
+ if (args.text) element.text = args.text;
559
+ if (args.style) element.style = args.style;
560
+ if (args.floatingActionButton) element.floatingActionButton = args.floatingActionButton;
561
+ if (args.placeholder) (element as any).placeholder = args.placeholder;
562
+ if (args.label) (element as any).label = args.label;
563
+ if (args.contentDescription) (element as any).contentDescription = args.contentDescription;
564
+
565
+ // Trailing lambda
566
+ if (this.check(TokenType.BraceOpen)) {
567
+ this.advance(); // {
568
+ element.children = this.parseBlockContent();
569
+ this.consume(TokenType.BraceClose, "Expect '}' after lambda");
570
+ }
571
+
572
+ return element;
573
+ }
574
+
575
+ private parseArguments(): any {
576
+ const args: any = {};
577
+
578
+ while (!this.check(TokenType.ParenClose) && !this.isAtEnd()) {
579
+ // Look for "name ="
580
+ if (this.check(TokenType.Identifier) && this.peek().type === TokenType.Equals) {
581
+ const argName = this.advance().value;
582
+ this.advance(); // consume '='
583
+
584
+ // Check if value is a block { ... }
585
+ if (this.check(TokenType.BraceOpen)) {
586
+ this.consume(TokenType.BraceOpen, "Expect '{'");
587
+ const blockElements = this.parseBlockContent();
588
+ this.consume(TokenType.BraceClose, "Expect '}'");
589
+
590
+ // Handle specific named block arguments
591
+ if (argName === 'floatingActionButton') {
592
+ args.floatingActionButton = blockElements.length > 0 ? blockElements[0] : null;
593
+ } else if (argName === 'topBar') {
594
+ args.topBar = blockElements.length > 0 ? blockElements[0] : null;
595
+ } else if (argName === 'bottomBar') {
596
+ args.bottomBar = blockElements.length > 0 ? blockElements[0] : null;
597
+ } else if (argName === 'placeholder') {
598
+ // Extract text from placeholder lambda: placeholder = { Text("...") }
599
+ if (blockElements.length > 0 && blockElements[0].text) {
600
+ args.placeholder = blockElements[0].text;
601
+ }
602
+ } else if (argName === 'label') {
603
+ // Extract text from label lambda: label = { Text("...") }
604
+ if (blockElements.length > 0 && blockElements[0].text) {
605
+ args.label = blockElements[0].text;
606
+ }
607
+ } else if (argName === 'title') {
608
+ // Extract title for AlertDialog: title = { Text("New Note") }
609
+ if (blockElements.length > 0 && blockElements[0].text) {
610
+ args.title = blockElements[0].text;
611
+ }
612
+ } else if (argName === 'text') {
613
+ // Dialog content lambda: text = { Column { ... } }
614
+ args.textContent = blockElements;
615
+ } else if (argName === 'confirmButton') {
616
+ // AlertDialog confirmButton lambda
617
+ args.confirmButton = blockElements.length > 0 ? blockElements[0] : null;
618
+ } else if (argName === 'dismissButton') {
619
+ // AlertDialog dismissButton lambda
620
+ args.dismissButton = blockElements.length > 0 ? blockElements[0] : null;
621
+ } else if (argName === 'content') {
622
+ args.contentChildren = blockElements;
623
+ } else if (argName === 'onClick') {
624
+ // Flag as interactive
625
+ args.onClick = "interaction";
626
+ } else {
627
+ // Generic: store any other lambda content
628
+ args[argName + 'Content'] = blockElements;
629
+ }
630
+ } else {
631
+ // Value parsing
632
+ const value = this.parseValue();
633
+
634
+ if (argName === 'text') args.text = value;
635
+ if (argName === 'modifier') args.modifier = value;
636
+ if (argName === 'style') args.style = value;
637
+ if (argName === 'contentDescription') args.contentDescription = value;
638
+ if (argName === 'value') args.value = value;
639
+ if (argName === 'minLines') args.minLines = value;
640
+ if (argName === 'onClick') args.onClick = "interaction"; // Handle onClick = ref
641
+ }
642
+ } else {
643
+ // Positional arg? Text("Foo")
644
+ // Assume first arg is content/text for Text components
645
+ const value = this.parseValue();
646
+ if (typeof value === 'string') args.text = value; // Heuristic
647
+ }
648
+
649
+ if (!this.match(TokenType.Comma)) {
650
+ break;
651
+ }
652
+ }
653
+
654
+ this.consume(TokenType.ParenClose, "Expect ')' after arguments");
655
+ return args;
656
+ }
657
+
658
+ private parseValue(): any {
659
+ if (this.match(TokenType.StringLiteral)) {
660
+ return this.previous().value;
661
+ }
662
+ if (this.match(TokenType.NumberLiteral)) {
663
+ // check for .dp
664
+ let num = parseFloat(this.previous().value);
665
+ if (this.match(TokenType.Dot) && this.check(TokenType.Identifier) && this.peekCurrent().value === 'dp') {
666
+ this.advance(); // consume dp
667
+ // It's a dp value, return raw number for DSL usually (dsl-types expectation)
668
+ // Modifier parsing logic handles this differently?
669
+ // If this is passed to 'height', we just want the number.
670
+ }
671
+ return num;
672
+ }
673
+
674
+ // Identifier chain: MaterialTheme.typography.bodyLarge or FunctionCall(args)
675
+ if (this.match(TokenType.Identifier)) {
676
+ let chain = this.previous().value;
677
+ while (this.match(TokenType.Dot)) {
678
+ if (this.match(TokenType.Identifier)) {
679
+ chain += '.' + this.previous().value;
680
+ }
681
+ }
682
+
683
+ // Handle method reference: viewModel::onSearchQueryChanged
684
+ if (this.check(TokenType.Operator) && this.peekCurrent().value === '::') {
685
+ this.advance(); // consume ::
686
+ if (this.match(TokenType.Identifier)) {
687
+ chain += '::' + this.previous().value;
688
+ }
689
+ return chain; // Return as string representation
690
+ }
691
+
692
+ // Modifier parsing: Modifier.padding(...).fillMaxSize()
693
+ if (chain.startsWith('Modifier')) {
694
+ return this.continueParsingModifier(chain);
695
+ }
696
+
697
+ // Check for function-like call: Color(0xFF...) or StaggeredGridCells.Fixed(2)
698
+ if (this.check(TokenType.ParenOpen)) {
699
+ this.consumeParentheses(); // Consume params cleanly
700
+ return chain + "(...)"; // Return simplified representation
701
+ }
702
+
703
+ // Style parsing: MaterialTheme.typography.displaySmall
704
+ if (chain.includes('typography')) {
705
+ return chain.split('.').pop(); // return "displaySmall"
706
+ }
707
+
708
+
709
+ // Detect variable references (data-bound content)
710
+ // We provide realistic mock data for common variable names to support "Visual Hot Reload"
711
+ // regardless of runtime data availability.
712
+ const firstChar = chain.charAt(0);
713
+ const isLowerCase = firstChar === firstChar.toLowerCase() && firstChar !== firstChar.toUpperCase();
714
+ const knownPrefixes = ['MaterialTheme', 'Icons', 'Color', 'Arrangement', 'Alignment', 'ContentScale', 'FontWeight', 'TextStyle'];
715
+ const isKnownConstant = knownPrefixes.some(p => chain.startsWith(p));
716
+
717
+ if (isLowerCase && !isKnownConstant) {
718
+ return `{{ ${chain} }}`;
719
+ }
720
+
721
+ return chain;
722
+ }
723
+
724
+ return null;
725
+ }
726
+
727
+ // REMOVE THIS DUPLICATE - it was already here
728
+ private parseValueOLD(): any {
729
+ if (this.match(TokenType.StringLiteral)) {
730
+ return this.previous().value;
731
+ }
732
+ if (this.match(TokenType.NumberLiteral)) {
733
+ let num = parseFloat(this.previous().value);
734
+ if (this.match(TokenType.Dot) && this.check(TokenType.Identifier) && this.peekCurrent().value === 'dp') {
735
+ this.advance();
736
+ }
737
+ return num;
738
+ }
739
+
740
+ if (this.match(TokenType.Identifier)) {
741
+ let chain = this.previous().value;
742
+ while (this.match(TokenType.Dot)) {
743
+ if (this.match(TokenType.Identifier)) {
744
+ chain += '.' + this.previous().value;
745
+ }
746
+ }
747
+
748
+ if (this.check(TokenType.Operator) && this.peekCurrent().value === '::') {
749
+ this.advance();
750
+ if (this.match(TokenType.Identifier)) {
751
+ chain += '::' + this.previous().value;
752
+ }
753
+ return chain;
754
+ }
755
+
756
+ if (chain.startsWith('Modifier')) {
757
+ return this.continueParsingModifier(chain);
758
+ }
759
+
760
+ if (this.check(TokenType.ParenOpen)) {
761
+ this.consumeParentheses();
762
+ return chain + "(...)";
763
+ }
764
+
765
+ if (chain.includes('typography')) {
766
+ return chain.split('.').pop(); // return "displaySmall"
767
+ }
768
+ return chain;
769
+ }
770
+
771
+ return null;
772
+ }
773
+
774
+ private consumeParentheses() {
775
+ if (this.match(TokenType.ParenOpen)) {
776
+ let nesting = 1;
777
+ while (nesting > 0 && !this.isAtEnd()) {
778
+ if (this.check(TokenType.ParenOpen)) nesting++;
779
+ if (this.check(TokenType.ParenClose)) nesting--;
780
+ this.advance();
781
+ }
782
+ }
783
+ }
784
+
785
+ private continueParsingModifier(initialChain: string): any {
786
+ const modifier: any = {};
787
+ let currentSegment = initialChain; // e.g. "Modifier.padding" or just "Modifier"
788
+
789
+ // Loop to handle chain
790
+ // usage: Modifier.padding(16.dp).fillMaxSize()
791
+
792
+ while (true) {
793
+ // Check for call (...)
794
+ if (this.match(TokenType.ParenOpen)) {
795
+ // Parsing arguments for this modifier method
796
+ if (currentSegment.endsWith('padding')) {
797
+ // Handle: padding(16.dp) or padding(start = 16.dp, top = 8.dp)
798
+ this.parseModifierPaddingArgs(modifier);
799
+ } else if (currentSegment.endsWith('height')) {
800
+ const val = this.parseValue();
801
+ if (typeof val === 'number') modifier.height = val;
802
+ while (!this.check(TokenType.ParenClose)) this.advance();
803
+ } else if (currentSegment.endsWith('width')) {
804
+ const val = this.parseValue();
805
+ if (typeof val === 'number') modifier.width = val;
806
+ while (!this.check(TokenType.ParenClose)) this.advance();
807
+ } else if (currentSegment.endsWith('size')) {
808
+ const val = this.parseValue();
809
+ if (typeof val === 'number') modifier.size = val;
810
+ while (!this.check(TokenType.ParenClose)) this.advance();
811
+ } else {
812
+ // unknown method, skip args
813
+ while (!this.check(TokenType.ParenClose)) this.advance();
814
+ }
815
+ this.consume(TokenType.ParenClose, "Expect ')'");
816
+ } else {
817
+ // property access or no-arg method?
818
+ // .fillMaxSize
819
+ if (currentSegment.endsWith('fillMaxSize')) modifier.fillMaxSize = true;
820
+ if (currentSegment.endsWith('fillMaxWidth')) modifier.fillMaxWidth = true;
821
+ if (currentSegment.endsWith('fillMaxHeight')) modifier.fillMaxHeight = true;
822
+ }
823
+
824
+ // Next in chain
825
+ if (this.match(TokenType.Dot)) {
826
+ if (this.match(TokenType.Identifier)) {
827
+ currentSegment = this.previous().value;
828
+ } else { break; }
829
+ } else {
830
+ break;
831
+ }
832
+ }
833
+
834
+ return modifier;
835
+ }
836
+
837
+ /**
838
+ * Parse padding arguments: padding(16.dp) or padding(start = 16.dp, top = 8.dp, horizontal = 16.dp)
839
+ */
840
+ private parseModifierPaddingArgs(modifier: any) {
841
+ // Check for named or positional arguments
842
+ while (!this.check(TokenType.ParenClose) && !this.isAtEnd()) {
843
+ if (this.check(TokenType.Identifier) && this.peek().type === TokenType.Equals) {
844
+ // Named argument: start = 16.dp
845
+ const argName = this.advance().value;
846
+ this.advance(); // consume '='
847
+ const val = this.parseValue();
848
+ if (typeof val === 'number') {
849
+ if (argName === 'start') modifier.paddingStart = val;
850
+ else if (argName === 'end') modifier.paddingEnd = val;
851
+ else if (argName === 'top') modifier.paddingTop = val;
852
+ else if (argName === 'bottom') modifier.paddingBottom = val;
853
+ else if (argName === 'horizontal') modifier.paddingHorizontal = val;
854
+ else if (argName === 'vertical') modifier.paddingVertical = val;
855
+ else if (argName === 'all') modifier.padding = val;
856
+ }
857
+ } else {
858
+ // Positional argument: padding(16.dp)
859
+ const val = this.parseValue();
860
+ if (typeof val === 'number') {
861
+ modifier.padding = val;
862
+ }
863
+ }
864
+
865
+ if (!this.match(TokenType.Comma)) break;
866
+ }
867
+ }
868
+
869
+ private skipDeclaration() {
870
+ // Skip until equals, then consume the expression
871
+ // val x = 10
872
+ // val x by remember { mutableStateOf(false) }
873
+ while (!this.isAtEnd() && !this.check(TokenType.Equals)) {
874
+ // Handle 'by' keyword for delegated properties
875
+ if (this.check(TokenType.Identifier) && this.peekCurrent().value === 'by') {
876
+ this.advance(); // consume 'by'
877
+ break;
878
+ }
879
+ this.advance();
880
+ }
881
+ if (this.match(TokenType.Equals)) {
882
+ this.consumeExpression();
883
+ } else {
884
+ // 'by' delegation - consume the expression
885
+ this.consumeExpression();
886
+ }
887
+ }
888
+
889
+ /**
890
+ * Skip an assignment statement: identifier = expression
891
+ * Handles: showAddDialog = true, x = foo.bar(), etc.
892
+ */
893
+ private skipAssignment() {
894
+ // We've already consumed the identifier, now consume = and expression
895
+ if (this.match(TokenType.Equals)) {
896
+ this.consumeExpression();
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Consume an expression, handling nested parentheses, braces, and brackets.
902
+ * Stops at: comma, closing paren/brace (unmatched), keywords that start statements, or EOF.
903
+ */
904
+ private consumeExpression() {
905
+ let parenDepth = 0;
906
+ let braceDepth = 0;
907
+
908
+ while (!this.isAtEnd()) {
909
+ const current = this.peekCurrent();
910
+
911
+ // Stop at keywords that start new statements (only at top level)
912
+ if (current.type === TokenType.Keyword && parenDepth === 0 && braceDepth === 0) {
913
+ const kw = current.value;
914
+ if (kw === 'val' || kw === 'var' || kw === 'fun' || kw === 'if' || kw === 'return' || kw === 'class' || kw === 'object') {
915
+ break; // New statement starting
916
+ }
917
+ }
918
+
919
+ // Stop at Identifiers that look like function calls (Composables) at top level
920
+ // Heuristic: Uppercase first letter followed by ( suggests a Composable call
921
+ if (current.type === TokenType.Identifier && parenDepth === 0 && braceDepth === 0) {
922
+ const firstChar = current.value.charAt(0);
923
+ if (firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase()) {
924
+ // Check if next token is ( or { (function call)
925
+ const nextIdx = this.position + 1;
926
+ if (nextIdx < this.tokens.length) {
927
+ const next = this.tokens[nextIdx];
928
+ if (next.type === TokenType.ParenOpen || next.type === TokenType.BraceOpen) {
929
+ break; // Likely a new Composable call statement
930
+ }
931
+ }
932
+ }
933
+ }
934
+
935
+ // Track nesting
936
+ if (current.type === TokenType.ParenOpen) {
937
+ parenDepth++;
938
+ this.advance();
939
+ continue;
940
+ }
941
+ if (current.type === TokenType.ParenClose) {
942
+ if (parenDepth === 0) break; // Unmatched - belongs to parent
943
+ parenDepth--;
944
+ this.advance();
945
+ continue;
946
+ }
947
+ if (current.type === TokenType.BraceOpen) {
948
+ braceDepth++;
949
+ this.advance();
950
+ continue;
951
+ }
952
+ if (current.type === TokenType.BraceClose) {
953
+ if (braceDepth === 0) break; // Unmatched - belongs to parent
954
+ braceDepth--;
955
+ this.advance();
956
+ continue;
957
+ }
958
+
959
+ // Stop at comma only if we're at top level (not nested)
960
+ if (current.type === TokenType.Comma && parenDepth === 0 && braceDepth === 0) {
961
+ break;
962
+ }
963
+
964
+ this.advance();
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Skip lambda parameters at the start of a block: { padding -> ... } or { a, b -> ... }
970
+ * Returns true if parameters were skipped.
971
+ */
972
+ private skipLambdaParameters(): boolean {
973
+ // Look ahead to detect pattern: identifier(s) followed by ->
974
+ // Arrow is tokenized as two operators: - and >
975
+ const startPos = this.position;
976
+
977
+ // Consume identifiers and commas
978
+ while (this.check(TokenType.Identifier)) {
979
+ this.advance();
980
+ if (!this.match(TokenType.Comma)) break;
981
+ }
982
+
983
+ // Check for arrow (- followed by >)
984
+ if (this.check(TokenType.Operator) && this.peekCurrent().value === '-') {
985
+ this.advance();
986
+ if (this.check(TokenType.Operator) && this.peekCurrent().value === '>') {
987
+ this.advance();
988
+ return true; // Successfully skipped lambda params
989
+ }
990
+ }
991
+
992
+ // No arrow found, restore position
993
+ this.position = startPos;
994
+ return false;
995
+ }
996
+
997
+ // Helpers
998
+ private match(type: TokenType): boolean {
999
+ if (this.check(type)) {
1000
+ this.advance();
1001
+ return true;
1002
+ }
1003
+ return false;
1004
+ }
1005
+
1006
+ private check(type: TokenType): boolean {
1007
+ if (this.isAtEnd()) return false;
1008
+ return this.peekCurrent().type === type;
1009
+ }
1010
+
1011
+ private advance(): Token {
1012
+ if (!this.isAtEnd()) this.position++;
1013
+ return this.previous();
1014
+ }
1015
+
1016
+ private isAtEnd(): boolean {
1017
+ return this.peekCurrent().type === TokenType.EOF;
1018
+ }
1019
+
1020
+ private peekCurrent(): Token {
1021
+ return this.tokens[this.position];
1022
+ }
1023
+
1024
+ private peek(): Token { // lookahead
1025
+ if (this.position + 1 >= this.tokens.length) return this.tokens[this.tokens.length - 1]; // EOF
1026
+ return this.tokens[this.position + 1];
1027
+ }
1028
+
1029
+ private previous(): Token {
1030
+ return this.tokens[this.position - 1];
1031
+ }
1032
+
1033
+ private consume(type: TokenType, message: string): Token {
1034
+ if (this.check(type)) return this.advance();
1035
+ throw new Error(`Parse Error: ${message} at line ${this.peekCurrent().line}`);
1036
+ }
1037
+
1038
+ private consumeUntil(type: TokenType) {
1039
+ while (!this.isAtEnd() && !this.check(type)) {
1040
+ this.advance();
1041
+ }
1042
+ }
1043
+ }