imxc 0.1.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/dist/components.d.ts +12 -0
- package/dist/components.js +208 -0
- package/dist/emitter.d.ts +12 -0
- package/dist/emitter.js +716 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +119 -0
- package/dist/ir.d.ts +193 -0
- package/dist/ir.js +1 -0
- package/dist/lowering.d.ts +17 -0
- package/dist/lowering.js +788 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.js +47 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +138 -0
- package/package.json +27 -0
package/dist/lowering.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { HOST_COMPONENTS, isHostComponent } from './components.js';
|
|
3
|
+
export function lowerComponent(parsed, validation) {
|
|
4
|
+
const func = parsed.component;
|
|
5
|
+
const name = func.name.text;
|
|
6
|
+
// Build state slots
|
|
7
|
+
const stateSlots = validation.useStateCalls.map(u => ({
|
|
8
|
+
name: u.name,
|
|
9
|
+
setter: u.setter,
|
|
10
|
+
type: inferTypeFromExpr(u.initializer),
|
|
11
|
+
initialValue: exprToLiteral(u.initializer),
|
|
12
|
+
index: u.index,
|
|
13
|
+
}));
|
|
14
|
+
// Build state lookup maps
|
|
15
|
+
const stateVars = new Map();
|
|
16
|
+
const setterMap = new Map();
|
|
17
|
+
for (const slot of stateSlots) {
|
|
18
|
+
stateVars.set(slot.name, slot);
|
|
19
|
+
setterMap.set(slot.setter, slot.name);
|
|
20
|
+
}
|
|
21
|
+
// Detect props parameter
|
|
22
|
+
let propsParam = null;
|
|
23
|
+
const params = [];
|
|
24
|
+
if (func.parameters.length > 0) {
|
|
25
|
+
const param = func.parameters[0];
|
|
26
|
+
if (ts.isIdentifier(param.name)) {
|
|
27
|
+
propsParam = param.name.text;
|
|
28
|
+
}
|
|
29
|
+
// Extract prop types from type annotation
|
|
30
|
+
if (param.type && ts.isTypeLiteralNode(param.type)) {
|
|
31
|
+
for (const member of param.type.members) {
|
|
32
|
+
if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
|
|
33
|
+
const propName = member.name.text;
|
|
34
|
+
const propType = inferPropType(member);
|
|
35
|
+
params.push({ name: propName, type: propType });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const ctx = {
|
|
41
|
+
stateVars,
|
|
42
|
+
setterMap,
|
|
43
|
+
propsParam,
|
|
44
|
+
bufferIndex: 0,
|
|
45
|
+
sourceFile: parsed.sourceFile,
|
|
46
|
+
};
|
|
47
|
+
// Find return statement and lower its JSX
|
|
48
|
+
const body = [];
|
|
49
|
+
if (func.body) {
|
|
50
|
+
const returnStmt = func.body.statements.find(ts.isReturnStatement);
|
|
51
|
+
if (returnStmt && returnStmt.expression) {
|
|
52
|
+
lowerJsxExpression(returnStmt.expression, body, ctx);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
stateSlots,
|
|
58
|
+
bufferCount: ctx.bufferIndex,
|
|
59
|
+
params,
|
|
60
|
+
body,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function inferPropType(member) {
|
|
64
|
+
if (!member.type)
|
|
65
|
+
return 'string';
|
|
66
|
+
if (ts.isFunctionTypeNode(member.type))
|
|
67
|
+
return 'callback';
|
|
68
|
+
const text = member.type.getText();
|
|
69
|
+
if (text === 'number')
|
|
70
|
+
return 'int';
|
|
71
|
+
if (text === 'boolean')
|
|
72
|
+
return 'bool';
|
|
73
|
+
if (text === 'string')
|
|
74
|
+
return 'string';
|
|
75
|
+
return 'string';
|
|
76
|
+
}
|
|
77
|
+
function inferTypeFromExpr(expr) {
|
|
78
|
+
if (ts.isNumericLiteral(expr)) {
|
|
79
|
+
// Use getText() to check original source text, since TS normalizes 5.0 to "5" in .text
|
|
80
|
+
const rawText = expr.getText();
|
|
81
|
+
return rawText.includes('.') ? 'float' : 'int';
|
|
82
|
+
}
|
|
83
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
|
|
84
|
+
return 'string';
|
|
85
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
86
|
+
return 'bool';
|
|
87
|
+
if (ts.isArrayLiteralExpression(expr))
|
|
88
|
+
return 'color';
|
|
89
|
+
// Default to int
|
|
90
|
+
return 'int';
|
|
91
|
+
}
|
|
92
|
+
function exprToLiteral(expr) {
|
|
93
|
+
if (ts.isNumericLiteral(expr)) {
|
|
94
|
+
// Use getText() to preserve original source (e.g., "5.0" instead of "5")
|
|
95
|
+
return expr.getText();
|
|
96
|
+
}
|
|
97
|
+
if (ts.isStringLiteral(expr))
|
|
98
|
+
return JSON.stringify(expr.text);
|
|
99
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword)
|
|
100
|
+
return 'true';
|
|
101
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
102
|
+
return 'false';
|
|
103
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
104
|
+
const elements = expr.elements.map(e => {
|
|
105
|
+
const text = e.getText();
|
|
106
|
+
// Ensure float suffix for color array elements
|
|
107
|
+
if (ts.isNumericLiteral(e)) {
|
|
108
|
+
return text.includes('.') ? `${text}f` : `${text}.0f`;
|
|
109
|
+
}
|
|
110
|
+
return text;
|
|
111
|
+
}).join(', ');
|
|
112
|
+
return `{${elements}}`;
|
|
113
|
+
}
|
|
114
|
+
// Fallback: use text as-is
|
|
115
|
+
return expr.getText();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Convert a TypeScript expression to C++ code string.
|
|
119
|
+
*/
|
|
120
|
+
export function exprToCpp(node, ctx) {
|
|
121
|
+
// Numeric literal
|
|
122
|
+
if (ts.isNumericLiteral(node)) {
|
|
123
|
+
return node.text;
|
|
124
|
+
}
|
|
125
|
+
// String literal
|
|
126
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
127
|
+
return JSON.stringify(node.text);
|
|
128
|
+
}
|
|
129
|
+
// Boolean literals
|
|
130
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
131
|
+
return 'true';
|
|
132
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
133
|
+
return 'false';
|
|
134
|
+
// Identifier
|
|
135
|
+
if (ts.isIdentifier(node)) {
|
|
136
|
+
const name = node.text;
|
|
137
|
+
if (ctx.stateVars.has(name)) {
|
|
138
|
+
return `${name}.get()`;
|
|
139
|
+
}
|
|
140
|
+
return name;
|
|
141
|
+
}
|
|
142
|
+
// Property access (e.g., props.name)
|
|
143
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
144
|
+
const obj = exprToCpp(node.expression, ctx);
|
|
145
|
+
const prop = node.name.text;
|
|
146
|
+
return `${obj}.${prop}`;
|
|
147
|
+
}
|
|
148
|
+
// Parenthesized expression
|
|
149
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
150
|
+
return `(${exprToCpp(node.expression, ctx)})`;
|
|
151
|
+
}
|
|
152
|
+
// Binary expression
|
|
153
|
+
if (ts.isBinaryExpression(node)) {
|
|
154
|
+
const left = exprToCpp(node.left, ctx);
|
|
155
|
+
const right = exprToCpp(node.right, ctx);
|
|
156
|
+
const op = node.operatorToken.getText();
|
|
157
|
+
return `${left} ${op} ${right}`;
|
|
158
|
+
}
|
|
159
|
+
// Prefix unary expression (e.g., !show)
|
|
160
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
161
|
+
const operand = exprToCpp(node.operand, ctx);
|
|
162
|
+
const opStr = node.operator === ts.SyntaxKind.ExclamationToken ? '!' :
|
|
163
|
+
node.operator === ts.SyntaxKind.MinusToken ? '-' :
|
|
164
|
+
node.operator === ts.SyntaxKind.PlusToken ? '+' : '';
|
|
165
|
+
return `${opStr}${operand}`;
|
|
166
|
+
}
|
|
167
|
+
// Call expression
|
|
168
|
+
if (ts.isCallExpression(node)) {
|
|
169
|
+
const callee = node.expression;
|
|
170
|
+
// Check for state setter: setCount(x) -> count.set(x)
|
|
171
|
+
if (ts.isIdentifier(callee)) {
|
|
172
|
+
const setterName = callee.text;
|
|
173
|
+
const stateVarName = ctx.setterMap.get(setterName);
|
|
174
|
+
if (stateVarName !== undefined) {
|
|
175
|
+
const arg = node.arguments.length > 0 ? exprToCpp(node.arguments[0], ctx) : '';
|
|
176
|
+
return `${stateVarName}.set(${arg})`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// General call expression
|
|
180
|
+
const fn = exprToCpp(callee, ctx);
|
|
181
|
+
const args = node.arguments.map(a => exprToCpp(a, ctx)).join(', ');
|
|
182
|
+
return `${fn}(${args})`;
|
|
183
|
+
}
|
|
184
|
+
// Conditional (ternary) expression: a ? b : c
|
|
185
|
+
if (ts.isConditionalExpression(node)) {
|
|
186
|
+
const condition = exprToCpp(node.condition, ctx);
|
|
187
|
+
const whenTrue = exprToCpp(node.whenTrue, ctx);
|
|
188
|
+
const whenFalse = exprToCpp(node.whenFalse, ctx);
|
|
189
|
+
return `${condition} ? ${whenTrue} : ${whenFalse}`;
|
|
190
|
+
}
|
|
191
|
+
// Arrow function -> C++ lambda
|
|
192
|
+
if (ts.isArrowFunction(node)) {
|
|
193
|
+
if (ts.isBlock(node.body)) {
|
|
194
|
+
const stmts = [];
|
|
195
|
+
for (const stmt of node.body.statements) {
|
|
196
|
+
stmts.push(stmtToCpp(stmt, ctx));
|
|
197
|
+
}
|
|
198
|
+
return `[&]() { ${stmts.join(' ')} }`;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const bodyExpr = exprToCpp(node.body, ctx);
|
|
202
|
+
return `[&]() { ${bodyExpr}; }`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Template literal
|
|
206
|
+
if (ts.isTemplateExpression(node)) {
|
|
207
|
+
let result = JSON.stringify(node.head.text);
|
|
208
|
+
for (const span of node.templateSpans) {
|
|
209
|
+
const expr = exprToCpp(span.expression, ctx);
|
|
210
|
+
result = `${result} + std::to_string(${expr}) + ${JSON.stringify(span.literal.text)}`;
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
// Array literal: ["a", "b"] -> "\"a\", \"b\""
|
|
215
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
216
|
+
return node.elements.map(e => exprToCpp(e, ctx)).join(', ');
|
|
217
|
+
}
|
|
218
|
+
// Fallback: use text representation
|
|
219
|
+
return node.getText();
|
|
220
|
+
}
|
|
221
|
+
function stmtToCpp(stmt, ctx) {
|
|
222
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
223
|
+
return exprToCpp(stmt.expression, ctx) + ';';
|
|
224
|
+
}
|
|
225
|
+
if (ts.isReturnStatement(stmt)) {
|
|
226
|
+
if (stmt.expression) {
|
|
227
|
+
return 'return ' + exprToCpp(stmt.expression, ctx) + ';';
|
|
228
|
+
}
|
|
229
|
+
return 'return;';
|
|
230
|
+
}
|
|
231
|
+
return stmt.getText() + ';';
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Extract action statements from an arrow function callback for button onPress etc.
|
|
235
|
+
*/
|
|
236
|
+
function extractActionStatements(expr, ctx) {
|
|
237
|
+
if (ts.isArrowFunction(expr)) {
|
|
238
|
+
if (ts.isBlock(expr.body)) {
|
|
239
|
+
return expr.body.statements.map(s => stmtToCpp(s, ctx));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Expression body: () => setCount(count + 1)
|
|
243
|
+
return [exprToCpp(expr.body, ctx) + ';'];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// If not an arrow function, just call it
|
|
247
|
+
return [exprToCpp(expr, ctx) + ';'];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Lower a JSX expression (possibly wrapped in parenthesized expr) into IR nodes.
|
|
251
|
+
*/
|
|
252
|
+
function lowerJsxExpression(node, body, ctx) {
|
|
253
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
254
|
+
lowerJsxExpression(node.expression, body, ctx);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (ts.isJsxElement(node)) {
|
|
258
|
+
lowerJsxElement(node, body, ctx);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
262
|
+
lowerJsxSelfClosing(node, body, ctx);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (ts.isJsxFragment(node)) {
|
|
266
|
+
for (const child of node.children) {
|
|
267
|
+
lowerJsxChild(child, body, ctx);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Conditional: condition && <Element/>
|
|
272
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
273
|
+
const condition = exprToCpp(node.left, ctx);
|
|
274
|
+
const condBody = [];
|
|
275
|
+
lowerJsxExpression(node.right, condBody, ctx);
|
|
276
|
+
body.push({ kind: 'conditional', condition, body: condBody });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Ternary: condition ? <A/> : <B/>
|
|
280
|
+
if (ts.isConditionalExpression(node)) {
|
|
281
|
+
const condition = exprToCpp(node.condition, ctx);
|
|
282
|
+
const thenBody = [];
|
|
283
|
+
const elseBody = [];
|
|
284
|
+
lowerJsxExpression(node.whenTrue, thenBody, ctx);
|
|
285
|
+
lowerJsxExpression(node.whenFalse, elseBody, ctx);
|
|
286
|
+
body.push({ kind: 'conditional', condition, body: thenBody, elseBody });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// items.map(item => <Comp/>)
|
|
290
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
|
|
291
|
+
lowerListMap(node, body, ctx);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// JsxExpression wrapper
|
|
295
|
+
if (ts.isJsxExpression(node) && node.expression) {
|
|
296
|
+
lowerJsxExpression(node.expression, body, ctx);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function lowerJsxElement(node, body, ctx) {
|
|
301
|
+
const tagName = node.openingElement.tagName;
|
|
302
|
+
if (!ts.isIdentifier(tagName))
|
|
303
|
+
return;
|
|
304
|
+
const name = tagName.text;
|
|
305
|
+
if (name === 'Text') {
|
|
306
|
+
lowerTextElement(node, body, ctx);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (isHostComponent(name)) {
|
|
310
|
+
const def = HOST_COMPONENTS[name];
|
|
311
|
+
const attrs = getAttributes(node.openingElement.attributes, ctx);
|
|
312
|
+
if (def.isContainer) {
|
|
313
|
+
const containerTag = name;
|
|
314
|
+
body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
|
|
315
|
+
for (const child of node.children) {
|
|
316
|
+
lowerJsxChild(child, body, ctx);
|
|
317
|
+
}
|
|
318
|
+
body.push({ kind: 'end_container', tag: containerTag });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Popup
|
|
322
|
+
if (name === 'Popup') {
|
|
323
|
+
const id = attrs['id'] ?? '';
|
|
324
|
+
body.push({ kind: 'begin_popup', id });
|
|
325
|
+
for (const child of node.children) {
|
|
326
|
+
lowerJsxChild(child, body, ctx);
|
|
327
|
+
}
|
|
328
|
+
body.push({ kind: 'end_popup' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Custom component with children - treat as container-like (not common but handle gracefully)
|
|
333
|
+
if (!isHostComponent(name)) {
|
|
334
|
+
lowerCustomComponent(name, node.openingElement.attributes, body, ctx);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function lowerJsxSelfClosing(node, body, ctx) {
|
|
339
|
+
const tagName = node.tagName;
|
|
340
|
+
if (!ts.isIdentifier(tagName))
|
|
341
|
+
return;
|
|
342
|
+
const name = tagName.text;
|
|
343
|
+
if (!isHostComponent(name)) {
|
|
344
|
+
lowerCustomComponent(name, node.attributes, body, ctx);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const attrs = getAttributes(node.attributes, ctx);
|
|
348
|
+
const rawAttrs = getRawAttributes(node.attributes);
|
|
349
|
+
switch (name) {
|
|
350
|
+
case 'Button':
|
|
351
|
+
lowerButton(attrs, rawAttrs, body, ctx);
|
|
352
|
+
break;
|
|
353
|
+
case 'TextInput':
|
|
354
|
+
lowerTextInput(attrs, rawAttrs, body, ctx);
|
|
355
|
+
break;
|
|
356
|
+
case 'Checkbox':
|
|
357
|
+
lowerCheckbox(attrs, rawAttrs, body, ctx);
|
|
358
|
+
break;
|
|
359
|
+
case 'MenuItem':
|
|
360
|
+
lowerMenuItem(attrs, rawAttrs, body, ctx);
|
|
361
|
+
break;
|
|
362
|
+
case 'SliderFloat':
|
|
363
|
+
lowerSliderFloat(attrs, rawAttrs, body, ctx);
|
|
364
|
+
break;
|
|
365
|
+
case 'SliderInt':
|
|
366
|
+
lowerSliderInt(attrs, rawAttrs, body, ctx);
|
|
367
|
+
break;
|
|
368
|
+
case 'DragFloat':
|
|
369
|
+
lowerDragFloat(attrs, rawAttrs, body, ctx);
|
|
370
|
+
break;
|
|
371
|
+
case 'DragInt':
|
|
372
|
+
lowerDragInt(attrs, rawAttrs, body, ctx);
|
|
373
|
+
break;
|
|
374
|
+
case 'Combo':
|
|
375
|
+
lowerCombo(attrs, rawAttrs, body, ctx);
|
|
376
|
+
break;
|
|
377
|
+
case 'InputInt':
|
|
378
|
+
lowerInputInt(attrs, rawAttrs, body, ctx);
|
|
379
|
+
break;
|
|
380
|
+
case 'InputFloat':
|
|
381
|
+
lowerInputFloat(attrs, rawAttrs, body, ctx);
|
|
382
|
+
break;
|
|
383
|
+
case 'ColorEdit':
|
|
384
|
+
lowerColorEdit(attrs, rawAttrs, body, ctx);
|
|
385
|
+
break;
|
|
386
|
+
case 'ListBox':
|
|
387
|
+
lowerListBox(attrs, rawAttrs, body, ctx);
|
|
388
|
+
break;
|
|
389
|
+
case 'ProgressBar':
|
|
390
|
+
lowerProgressBar(attrs, rawAttrs, body, ctx);
|
|
391
|
+
break;
|
|
392
|
+
case 'Tooltip':
|
|
393
|
+
lowerTooltip(attrs, body, ctx);
|
|
394
|
+
break;
|
|
395
|
+
case 'Separator':
|
|
396
|
+
body.push({ kind: 'separator' });
|
|
397
|
+
break;
|
|
398
|
+
case 'Text':
|
|
399
|
+
// Self-closing <Text /> - empty text
|
|
400
|
+
body.push({ kind: 'text', format: '', args: [] });
|
|
401
|
+
break;
|
|
402
|
+
default:
|
|
403
|
+
// Container self-closing (e.g., <Window title="X"/>)
|
|
404
|
+
if (HOST_COMPONENTS[name]?.isContainer) {
|
|
405
|
+
const containerTag = name;
|
|
406
|
+
body.push({ kind: 'begin_container', tag: containerTag, props: attrs });
|
|
407
|
+
body.push({ kind: 'end_container', tag: containerTag });
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function lowerButton(attrs, rawAttrs, body, ctx) {
|
|
413
|
+
const title = attrs['title'] ?? '""';
|
|
414
|
+
const onPressExpr = rawAttrs.get('onPress');
|
|
415
|
+
let action = [];
|
|
416
|
+
if (onPressExpr) {
|
|
417
|
+
action = extractActionStatements(onPressExpr, ctx);
|
|
418
|
+
}
|
|
419
|
+
const style = attrs['style'];
|
|
420
|
+
body.push({ kind: 'button', title, action, style });
|
|
421
|
+
}
|
|
422
|
+
function lowerTextInput(attrs, rawAttrs, body, ctx) {
|
|
423
|
+
const label = attrs['label'] ?? '""';
|
|
424
|
+
const bufferIndex = ctx.bufferIndex++;
|
|
425
|
+
// Detect bound state variable from value prop
|
|
426
|
+
let stateVar = '';
|
|
427
|
+
const valueExpr = rawAttrs.get('value');
|
|
428
|
+
if (valueExpr && ts.isIdentifier(valueExpr)) {
|
|
429
|
+
const varName = valueExpr.text;
|
|
430
|
+
if (ctx.stateVars.has(varName)) {
|
|
431
|
+
stateVar = varName;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const style = attrs['style'];
|
|
435
|
+
body.push({ kind: 'text_input', label, bufferIndex, stateVar, style });
|
|
436
|
+
}
|
|
437
|
+
function lowerCheckbox(attrs, rawAttrs, body, ctx) {
|
|
438
|
+
const label = attrs['label'] ?? '""';
|
|
439
|
+
// Detect bound state variable from value prop
|
|
440
|
+
let stateVar = '';
|
|
441
|
+
let valueExprStr;
|
|
442
|
+
let onChangeExprStr;
|
|
443
|
+
const valueExpr = rawAttrs.get('value');
|
|
444
|
+
if (valueExpr && ts.isIdentifier(valueExpr)) {
|
|
445
|
+
const varName = valueExpr.text;
|
|
446
|
+
if (ctx.stateVars.has(varName)) {
|
|
447
|
+
stateVar = varName;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Non-state value (e.g., props.done passed as identifier)
|
|
451
|
+
valueExprStr = exprToCpp(valueExpr, ctx);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else if (valueExpr) {
|
|
455
|
+
// Non-state value expression (e.g., props.done)
|
|
456
|
+
valueExprStr = exprToCpp(valueExpr, ctx);
|
|
457
|
+
}
|
|
458
|
+
// If not state-bound, get onChange expression
|
|
459
|
+
if (!stateVar) {
|
|
460
|
+
const onChangeRaw = rawAttrs.get('onChange');
|
|
461
|
+
if (onChangeRaw) {
|
|
462
|
+
onChangeExprStr = exprToCpp(onChangeRaw, ctx);
|
|
463
|
+
// If it's not already a lambda/call, make it a call
|
|
464
|
+
if (!onChangeExprStr.startsWith('[') && !onChangeExprStr.endsWith(')')) {
|
|
465
|
+
onChangeExprStr = `${onChangeExprStr}()`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const style = attrs['style'];
|
|
470
|
+
body.push({ kind: 'checkbox', label, stateVar, valueExpr: valueExprStr, onChangeExpr: onChangeExprStr, style });
|
|
471
|
+
}
|
|
472
|
+
function lowerMenuItem(attrs, rawAttrs, body, ctx) {
|
|
473
|
+
const label = attrs['label'] ?? '""';
|
|
474
|
+
const shortcut = attrs['shortcut'];
|
|
475
|
+
const onPressExpr = rawAttrs.get('onPress');
|
|
476
|
+
let action = [];
|
|
477
|
+
if (onPressExpr) {
|
|
478
|
+
action = extractActionStatements(onPressExpr, ctx);
|
|
479
|
+
}
|
|
480
|
+
body.push({ kind: 'menu_item', label, shortcut, action });
|
|
481
|
+
}
|
|
482
|
+
function lowerTextElement(node, body, ctx) {
|
|
483
|
+
let format = '';
|
|
484
|
+
const args = [];
|
|
485
|
+
for (const child of node.children) {
|
|
486
|
+
if (ts.isJsxText(child)) {
|
|
487
|
+
// Collapse whitespace (newlines, tabs, runs of spaces) into single spaces,
|
|
488
|
+
// matching JSX semantics. Only fully-blank segments are dropped.
|
|
489
|
+
const text = child.text.replace(/%/g, '%%').replace(/\s+/g, ' ');
|
|
490
|
+
// Drop segments that are purely whitespace at the very start or end of children
|
|
491
|
+
const isFirst = child === node.children[0];
|
|
492
|
+
const isLast = child === node.children[node.children.length - 1];
|
|
493
|
+
const trimmed = isFirst && isLast ? text.trim()
|
|
494
|
+
: isFirst ? text.trimStart()
|
|
495
|
+
: isLast ? text.trimEnd()
|
|
496
|
+
: text;
|
|
497
|
+
if (trimmed)
|
|
498
|
+
format += trimmed;
|
|
499
|
+
}
|
|
500
|
+
else if (ts.isJsxExpression(child) && child.expression) {
|
|
501
|
+
const expr = child.expression;
|
|
502
|
+
const cppExpr = exprToCpp(expr, ctx);
|
|
503
|
+
const exprType = inferExprType(expr, ctx);
|
|
504
|
+
switch (exprType) {
|
|
505
|
+
case 'int':
|
|
506
|
+
format += '%d';
|
|
507
|
+
args.push(cppExpr);
|
|
508
|
+
break;
|
|
509
|
+
case 'float':
|
|
510
|
+
format += '%.2f';
|
|
511
|
+
args.push(cppExpr);
|
|
512
|
+
break;
|
|
513
|
+
case 'bool':
|
|
514
|
+
format += '%s';
|
|
515
|
+
args.push(`${cppExpr} ? "true" : "false"`);
|
|
516
|
+
break;
|
|
517
|
+
case 'string':
|
|
518
|
+
format += '%s';
|
|
519
|
+
// String literals and ternaries of literals are already const char*
|
|
520
|
+
if (cppExpr.startsWith('"') || isCharPtrExpression(expr)) {
|
|
521
|
+
args.push(cppExpr);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
args.push(`${cppExpr}.c_str()`);
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
default:
|
|
528
|
+
format += '%s';
|
|
529
|
+
args.push(`std::to_string(${cppExpr}).c_str()`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
body.push({ kind: 'text', format, args });
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Check if an expression will produce a const char* in C++ (not std::string).
|
|
538
|
+
* String literals and ternaries where both branches are string literals qualify.
|
|
539
|
+
*/
|
|
540
|
+
function isCharPtrExpression(expr) {
|
|
541
|
+
if (ts.isStringLiteral(expr))
|
|
542
|
+
return true;
|
|
543
|
+
if (ts.isConditionalExpression(expr)) {
|
|
544
|
+
return isCharPtrExpression(expr.whenTrue) && isCharPtrExpression(expr.whenFalse);
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
function inferExprType(expr, ctx) {
|
|
549
|
+
if (ts.isNumericLiteral(expr)) {
|
|
550
|
+
return expr.text.includes('.') ? 'float' : 'int';
|
|
551
|
+
}
|
|
552
|
+
if (ts.isStringLiteral(expr))
|
|
553
|
+
return 'string';
|
|
554
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword)
|
|
555
|
+
return 'bool';
|
|
556
|
+
// Identifier: check state var type
|
|
557
|
+
if (ts.isIdentifier(expr)) {
|
|
558
|
+
const slot = ctx.stateVars.get(expr.text);
|
|
559
|
+
if (slot)
|
|
560
|
+
return slot.type;
|
|
561
|
+
}
|
|
562
|
+
// Property access: props.name -> look for string by default
|
|
563
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
564
|
+
return 'string';
|
|
565
|
+
}
|
|
566
|
+
// Binary expression: infer from operands
|
|
567
|
+
if (ts.isBinaryExpression(expr)) {
|
|
568
|
+
const op = expr.operatorToken.kind;
|
|
569
|
+
// Comparison operators return bool
|
|
570
|
+
if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
571
|
+
op === ts.SyntaxKind.ExclamationEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
572
|
+
op === ts.SyntaxKind.LessThanToken || op === ts.SyntaxKind.LessThanEqualsToken ||
|
|
573
|
+
op === ts.SyntaxKind.GreaterThanToken || op === ts.SyntaxKind.GreaterThanEqualsToken ||
|
|
574
|
+
op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
|
|
575
|
+
return 'bool';
|
|
576
|
+
}
|
|
577
|
+
// Arithmetic: infer from left side
|
|
578
|
+
return inferExprType(expr.left, ctx);
|
|
579
|
+
}
|
|
580
|
+
// Conditional (ternary): infer from the result branches
|
|
581
|
+
if (ts.isConditionalExpression(expr)) {
|
|
582
|
+
return inferExprType(expr.whenTrue, ctx);
|
|
583
|
+
}
|
|
584
|
+
// Call expression that is a state getter
|
|
585
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
586
|
+
const name = expr.expression.text;
|
|
587
|
+
if (ctx.setterMap.has(name)) {
|
|
588
|
+
const stateVarName = ctx.setterMap.get(name);
|
|
589
|
+
const slot = ctx.stateVars.get(stateVarName);
|
|
590
|
+
if (slot)
|
|
591
|
+
return slot.type;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return 'int'; // default
|
|
595
|
+
}
|
|
596
|
+
function lowerListMap(node, body, ctx) {
|
|
597
|
+
const propAccess = node.expression;
|
|
598
|
+
const array = exprToCpp(propAccess.expression, ctx);
|
|
599
|
+
const callback = node.arguments[0];
|
|
600
|
+
let itemVar = 'item';
|
|
601
|
+
let mapBody = [];
|
|
602
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
603
|
+
if (callback.parameters.length > 0 && ts.isIdentifier(callback.parameters[0].name)) {
|
|
604
|
+
itemVar = callback.parameters[0].name.text;
|
|
605
|
+
}
|
|
606
|
+
if (ts.isBlock(callback.body)) {
|
|
607
|
+
const ret = callback.body.statements.find(ts.isReturnStatement);
|
|
608
|
+
if (ret?.expression) {
|
|
609
|
+
lowerJsxExpression(ret.expression, mapBody, ctx);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else if (callback.body) {
|
|
613
|
+
lowerJsxExpression(callback.body, mapBody, ctx);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
body.push({
|
|
617
|
+
kind: 'list_map',
|
|
618
|
+
array,
|
|
619
|
+
itemVar,
|
|
620
|
+
key: 'i',
|
|
621
|
+
componentName: 'ListItem',
|
|
622
|
+
stateCount: 0,
|
|
623
|
+
bufferCount: 0,
|
|
624
|
+
body: mapBody,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function lowerCustomComponent(name, attributes, body, ctx) {
|
|
628
|
+
const attrs = getAttributes(attributes, ctx);
|
|
629
|
+
body.push({
|
|
630
|
+
kind: 'custom_component',
|
|
631
|
+
name,
|
|
632
|
+
props: attrs,
|
|
633
|
+
stateCount: 0,
|
|
634
|
+
bufferCount: 0,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function lowerJsxChild(child, body, ctx) {
|
|
638
|
+
if (ts.isJsxElement(child)) {
|
|
639
|
+
lowerJsxElement(child, body, ctx);
|
|
640
|
+
}
|
|
641
|
+
else if (ts.isJsxSelfClosingElement(child)) {
|
|
642
|
+
lowerJsxSelfClosing(child, body, ctx);
|
|
643
|
+
}
|
|
644
|
+
else if (ts.isJsxExpression(child)) {
|
|
645
|
+
if (child.expression) {
|
|
646
|
+
lowerJsxExpression(child.expression, body, ctx);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else if (ts.isJsxText(child)) {
|
|
650
|
+
// Standalone text not inside <Text> — usually whitespace, skip
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function getAttributes(attributes, ctx) {
|
|
654
|
+
const result = {};
|
|
655
|
+
for (const attr of attributes.properties) {
|
|
656
|
+
if (ts.isJsxAttribute(attr) && attr.name && ts.isIdentifier(attr.name)) {
|
|
657
|
+
const name = attr.name.text;
|
|
658
|
+
if (attr.initializer) {
|
|
659
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
660
|
+
result[name] = JSON.stringify(attr.initializer.text);
|
|
661
|
+
}
|
|
662
|
+
else if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
663
|
+
result[name] = exprToCpp(attr.initializer.expression, ctx);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
// Boolean shorthand: <X disabled /> means disabled={true}
|
|
668
|
+
result[name] = 'true';
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
function getRawAttributes(attributes) {
|
|
675
|
+
const result = new Map();
|
|
676
|
+
for (const attr of attributes.properties) {
|
|
677
|
+
if (ts.isJsxAttribute(attr) && attr.name && ts.isIdentifier(attr.name)) {
|
|
678
|
+
const name = attr.name.text;
|
|
679
|
+
if (attr.initializer && ts.isJsxExpression(attr.initializer)) {
|
|
680
|
+
result.set(name, attr.initializer.expression ?? null);
|
|
681
|
+
}
|
|
682
|
+
else if (attr.initializer && ts.isStringLiteral(attr.initializer)) {
|
|
683
|
+
result.set(name, attr.initializer);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
result.set(name, null);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
function lowerValueOnChange(rawAttrs, ctx) {
|
|
693
|
+
let stateVar = '';
|
|
694
|
+
let valueExpr;
|
|
695
|
+
let onChangeExpr;
|
|
696
|
+
const valueRaw = rawAttrs.get('value');
|
|
697
|
+
if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
|
|
698
|
+
stateVar = valueRaw.text;
|
|
699
|
+
}
|
|
700
|
+
else if (valueRaw) {
|
|
701
|
+
valueExpr = exprToCpp(valueRaw, ctx);
|
|
702
|
+
const onChangeRaw = rawAttrs.get('onChange');
|
|
703
|
+
if (onChangeRaw) {
|
|
704
|
+
onChangeExpr = exprToCpp(onChangeRaw, ctx);
|
|
705
|
+
if (!onChangeExpr.startsWith('[') && !onChangeExpr.endsWith(')')) {
|
|
706
|
+
onChangeExpr = `${onChangeExpr}()`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return { stateVar, valueExpr, onChangeExpr };
|
|
711
|
+
}
|
|
712
|
+
function lowerSliderFloat(attrs, rawAttrs, body, ctx) {
|
|
713
|
+
const label = attrs['label'] ?? '""';
|
|
714
|
+
const min = attrs['min'] ?? '0.0f';
|
|
715
|
+
const max = attrs['max'] ?? '1.0f';
|
|
716
|
+
const style = attrs['style'];
|
|
717
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
718
|
+
body.push({ kind: 'slider_float', label, stateVar, valueExpr, onChangeExpr, min, max, style });
|
|
719
|
+
}
|
|
720
|
+
function lowerSliderInt(attrs, rawAttrs, body, ctx) {
|
|
721
|
+
const label = attrs['label'] ?? '""';
|
|
722
|
+
const min = attrs['min'] ?? '0';
|
|
723
|
+
const max = attrs['max'] ?? '100';
|
|
724
|
+
const style = attrs['style'];
|
|
725
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
726
|
+
body.push({ kind: 'slider_int', label, stateVar, valueExpr, onChangeExpr, min, max, style });
|
|
727
|
+
}
|
|
728
|
+
function lowerDragFloat(attrs, rawAttrs, body, ctx) {
|
|
729
|
+
const label = attrs['label'] ?? '""';
|
|
730
|
+
const speed = attrs['speed'] ?? '1.0f';
|
|
731
|
+
const style = attrs['style'];
|
|
732
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
733
|
+
body.push({ kind: 'drag_float', label, stateVar, valueExpr, onChangeExpr, speed, style });
|
|
734
|
+
}
|
|
735
|
+
function lowerDragInt(attrs, rawAttrs, body, ctx) {
|
|
736
|
+
const label = attrs['label'] ?? '""';
|
|
737
|
+
const speed = attrs['speed'] ?? '1.0f';
|
|
738
|
+
const style = attrs['style'];
|
|
739
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
740
|
+
body.push({ kind: 'drag_int', label, stateVar, valueExpr, onChangeExpr, speed, style });
|
|
741
|
+
}
|
|
742
|
+
function lowerCombo(attrs, rawAttrs, body, ctx) {
|
|
743
|
+
const label = attrs['label'] ?? '""';
|
|
744
|
+
const items = attrs['items'] ?? '';
|
|
745
|
+
const style = attrs['style'];
|
|
746
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
747
|
+
body.push({ kind: 'combo', label, stateVar, valueExpr, onChangeExpr, items, style });
|
|
748
|
+
}
|
|
749
|
+
function lowerInputInt(attrs, rawAttrs, body, ctx) {
|
|
750
|
+
const label = attrs['label'] ?? '""';
|
|
751
|
+
const style = attrs['style'];
|
|
752
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
753
|
+
body.push({ kind: 'input_int', label, stateVar, valueExpr, onChangeExpr, style });
|
|
754
|
+
}
|
|
755
|
+
function lowerInputFloat(attrs, rawAttrs, body, ctx) {
|
|
756
|
+
const label = attrs['label'] ?? '""';
|
|
757
|
+
const style = attrs['style'];
|
|
758
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
759
|
+
body.push({ kind: 'input_float', label, stateVar, valueExpr, onChangeExpr, style });
|
|
760
|
+
}
|
|
761
|
+
function lowerColorEdit(attrs, rawAttrs, body, ctx) {
|
|
762
|
+
const label = attrs['label'] ?? '""';
|
|
763
|
+
const style = attrs['style'];
|
|
764
|
+
// ColorEdit only supports state-bound values
|
|
765
|
+
let stateVar = '';
|
|
766
|
+
const valueRaw = rawAttrs.get('value');
|
|
767
|
+
if (valueRaw && ts.isIdentifier(valueRaw) && ctx.stateVars.has(valueRaw.text)) {
|
|
768
|
+
stateVar = valueRaw.text;
|
|
769
|
+
}
|
|
770
|
+
body.push({ kind: 'color_edit', label, stateVar, style });
|
|
771
|
+
}
|
|
772
|
+
function lowerListBox(attrs, rawAttrs, body, ctx) {
|
|
773
|
+
const label = attrs['label'] ?? '""';
|
|
774
|
+
const items = attrs['items'] ?? '';
|
|
775
|
+
const style = attrs['style'];
|
|
776
|
+
const { stateVar, valueExpr, onChangeExpr } = lowerValueOnChange(rawAttrs, ctx);
|
|
777
|
+
body.push({ kind: 'list_box', label, stateVar, valueExpr, onChangeExpr, items, style });
|
|
778
|
+
}
|
|
779
|
+
function lowerProgressBar(attrs, rawAttrs, body, ctx) {
|
|
780
|
+
const value = attrs['value'] ?? '0.0f';
|
|
781
|
+
const overlay = attrs['overlay'];
|
|
782
|
+
const style = attrs['style'];
|
|
783
|
+
body.push({ kind: 'progress_bar', value, overlay, style });
|
|
784
|
+
}
|
|
785
|
+
function lowerTooltip(attrs, body, ctx) {
|
|
786
|
+
const text = attrs['text'] ?? '""';
|
|
787
|
+
body.push({ kind: 'tooltip', text });
|
|
788
|
+
}
|