@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.
- package/README.md +189 -74
- package/dist/build/dex-generator.d.ts +27 -0
- package/dist/build/dex-generator.js +202 -0
- package/dist/build/dsl-parser.d.ts +3 -30
- package/dist/build/dsl-parser.js +67 -240
- package/dist/build/dsl-types.d.ts +8 -0
- package/dist/build/gradle.d.ts +51 -0
- package/dist/build/gradle.js +233 -1
- package/dist/build/hot-reload-service.d.ts +36 -0
- package/dist/build/hot-reload-service.js +179 -0
- package/dist/build/js-compiler-service.d.ts +61 -0
- package/dist/build/js-compiler-service.js +421 -0
- package/dist/build/kotlin-compiler.d.ts +54 -0
- package/dist/build/kotlin-compiler.js +450 -0
- package/dist/build/kotlin-parser.d.ts +91 -0
- package/dist/build/kotlin-parser.js +1030 -0
- package/dist/build/override-generator.d.ts +54 -0
- package/dist/build/override-generator.js +430 -0
- package/dist/server/index.d.ts +16 -1
- package/dist/server/index.js +147 -42
- package/dist/websocket/handler.d.ts +20 -4
- package/dist/websocket/handler.js +73 -38
- package/dist/websocket/index.d.ts +8 -0
- package/dist/websocket/index.js +15 -11
- package/dist/websocket/manager.d.ts +2 -2
- package/dist/websocket/manager.js +1 -1
- package/package.json +3 -3
- package/src/build/dex-generator.ts +197 -0
- package/src/build/dsl-parser.ts +73 -272
- package/src/build/dsl-types.ts +9 -0
- package/src/build/gradle.ts +259 -1
- package/src/build/hot-reload-service.ts +178 -0
- package/src/build/js-compiler-service.ts +411 -0
- package/src/build/kotlin-compiler.ts +460 -0
- package/src/build/kotlin-parser.ts +1043 -0
- package/src/build/override-generator.ts +478 -0
- package/src/server/index.ts +162 -54
- package/src/websocket/handler.ts +94 -56
- package/src/websocket/index.ts +27 -14
- 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
|
+
}
|