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