@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,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
|