english-lang 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.
@@ -0,0 +1,657 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // ReactNativeBackend.ts — Emits React Native TypeScript from AST
4
+ //
5
+ // Generates clean, standard TypeScript that a senior developer
6
+ // would write by hand. Same .eng input → same output every time.
7
+ // ============================================================
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ReactNativeBackend = void 0;
10
+ // ─────────────────────────────────────────────────────────────
11
+ class ReactNativeBackend {
12
+ constructor() {
13
+ this.out = [];
14
+ this.depth = 0;
15
+ }
16
+ // ── Public entry point ────────────────────────────────────
17
+ emit(program, sourceFile) {
18
+ this.out = [];
19
+ this.depth = 0;
20
+ for (const screen of program.screens) {
21
+ this.emitScreen(screen, sourceFile, program.types);
22
+ this.line('');
23
+ }
24
+ for (const comp of program.components) {
25
+ this.emitComponent(comp, sourceFile);
26
+ this.line('');
27
+ }
28
+ return this.out.join('\n');
29
+ }
30
+ // ── Type declarations (noun) ──────────────────────────────
31
+ emitTypeDecl(type) {
32
+ this.line(`export interface ${type.name} {`);
33
+ this.depth++;
34
+ for (const field of type.fields) {
35
+ const tsType = this.tsTypeFromAnnotation(field.type);
36
+ this.line(`${field.name}: ${tsType};`);
37
+ }
38
+ this.depth--;
39
+ this.line('}');
40
+ this.line('');
41
+ }
42
+ // ── Screen ────────────────────────────────────────────────
43
+ emitScreen(screen, sourceFile, types = []) {
44
+ const stateVarTypes = this.inferStateTypes(screen.state);
45
+ const buttonHandlers = this.buildButtonHandlerMap(screen.events);
46
+ const needsEffect = screen.events.some(e => e.trigger.kind === 'ScreenOpens' || e.trigger.kind === 'ScreenCloses');
47
+ const needsState = screen.state.length > 0;
48
+ // Header
49
+ this.line(`// Generated from ${sourceFile}`);
50
+ this.line(`// Do not edit — modify the .eng source instead.`);
51
+ this.line('');
52
+ // Imports
53
+ const hooks = [];
54
+ if (needsState)
55
+ hooks.push('useState');
56
+ if (needsEffect)
57
+ hooks.push('useEffect');
58
+ this.line(`import React${hooks.length ? `, { ${hooks.join(', ')} }` : ''} from 'react';`);
59
+ const rnImports = this.collectRNImports(screen.layout, screen.events);
60
+ if (rnImports.length) {
61
+ this.line(`import { ${rnImports.join(', ')} } from 'react-native';`);
62
+ }
63
+ this.line('');
64
+ // Type interfaces (noun declarations)
65
+ for (const type of types) {
66
+ this.emitTypeDecl(type);
67
+ }
68
+ // Component function
69
+ this.line(`export function ${screen.name}({ navigation }: any) {`);
70
+ this.depth++;
71
+ // useState hooks
72
+ for (const field of screen.state) {
73
+ const tsType = this.tsType(field, stateVarTypes);
74
+ const initVal = this.emitExpr(field.defaultValue);
75
+ const setter = setterName(field.name);
76
+ this.line(`const [${field.name}, ${setter}] = useState<${tsType}>(${initVal});`);
77
+ }
78
+ if (screen.state.length)
79
+ this.line('');
80
+ // useEffect for screen opens
81
+ const screenOpensHandler = screen.events.find(e => e.trigger.kind === 'ScreenOpens');
82
+ if (screenOpensHandler) {
83
+ this.line('useEffect(() => {');
84
+ this.depth++;
85
+ for (const stmt of screenOpensHandler.body) {
86
+ this.emitStatement(stmt, stateVarTypes);
87
+ }
88
+ this.depth--;
89
+ this.line('}, []);');
90
+ this.line('');
91
+ }
92
+ // Button handler functions
93
+ for (const event of screen.events) {
94
+ if (event.trigger.kind !== 'ButtonPressed')
95
+ continue;
96
+ const fnName = buttonHandlers.get(event.trigger.label);
97
+ this.line(`const ${fnName} = () => {`);
98
+ this.depth++;
99
+ for (const stmt of event.body) {
100
+ this.emitStatement(stmt, stateVarTypes);
101
+ }
102
+ this.depth--;
103
+ this.line('};');
104
+ this.line('');
105
+ }
106
+ // Action functions
107
+ for (const action of screen.actions) {
108
+ this.emitAction(action, stateVarTypes);
109
+ }
110
+ // JSX return
111
+ this.line('return (');
112
+ this.depth++;
113
+ this.line('<View style={styles.container}>');
114
+ this.depth++;
115
+ if (screen.layout.length === 0) {
116
+ this.line('{/* empty screen */}');
117
+ }
118
+ else if (screen.layout.length === 1) {
119
+ this.emitLayoutNode(screen.layout[0], buttonHandlers, stateVarTypes);
120
+ }
121
+ else {
122
+ this.line('<View>');
123
+ this.depth++;
124
+ for (const node of screen.layout) {
125
+ this.emitLayoutNode(node, buttonHandlers, stateVarTypes);
126
+ }
127
+ this.depth--;
128
+ this.line('</View>');
129
+ }
130
+ this.depth--;
131
+ this.line('</View>');
132
+ this.depth--;
133
+ this.line(');');
134
+ this.depth--;
135
+ this.line('}');
136
+ this.line('');
137
+ // StyleSheet
138
+ this.emitStyleSheet();
139
+ }
140
+ // ── Component ─────────────────────────────────────────────
141
+ emitComponent(comp, sourceFile) {
142
+ const paramList = comp.params.map(p => `${p.name}: ${this.tsTypeFromAnnotation(p.type)}`).join(', ');
143
+ const propsType = comp.params.length ? `{ ${paramList} }` : 'Record<string, never>';
144
+ const buttonHandlers = this.buildButtonHandlerMap(comp.events);
145
+ const stateVarTypes = new Map();
146
+ this.line(`export function ${comp.name}({ ${comp.params.map(p => p.name).join(', ')} }: ${propsType}) {`);
147
+ this.depth++;
148
+ this.line('return (');
149
+ this.depth++;
150
+ if (comp.layout.length === 1) {
151
+ this.emitLayoutNode(comp.layout[0], buttonHandlers, stateVarTypes);
152
+ }
153
+ else {
154
+ this.line('<View>');
155
+ this.depth++;
156
+ for (const node of comp.layout) {
157
+ this.emitLayoutNode(node, buttonHandlers, stateVarTypes);
158
+ }
159
+ this.depth--;
160
+ this.line('</View>');
161
+ }
162
+ this.depth--;
163
+ this.line(');');
164
+ this.depth--;
165
+ this.line('}');
166
+ }
167
+ // ── Action functions ──────────────────────────────────────
168
+ emitAction(action, stateVarTypes) {
169
+ const fnName = camelCase(action.verbPhrase);
170
+ const params = action.params.map(p => `${p.name}: ${this.tsTypeFromAnnotation(p.type)}`).join(', ');
171
+ const retType = action.returnType ? `: ${this.tsTypeFromAnnotation(action.returnType)}` : '';
172
+ this.line(`const ${fnName} = async (${params})${retType} => {`);
173
+ this.depth++;
174
+ for (const stmt of action.body) {
175
+ this.emitStatement(stmt, stateVarTypes);
176
+ }
177
+ this.depth--;
178
+ this.line('};');
179
+ this.line('');
180
+ }
181
+ // ── Statements ────────────────────────────────────────────
182
+ emitStatement(stmt, stateVars) {
183
+ switch (stmt.kind) {
184
+ case 'SetStatement': {
185
+ const setter = stateVars.has(stmt.name) ? `${setterName(stmt.name)}` : stmt.name;
186
+ const val = this.emitExpr(stmt.value);
187
+ if (stateVars.has(stmt.name)) {
188
+ this.line(`${setter}(${val});`);
189
+ }
190
+ else {
191
+ this.line(`${stmt.name} = ${val};`);
192
+ }
193
+ break;
194
+ }
195
+ case 'FetchStatement': {
196
+ this.line(`try {`);
197
+ this.depth++;
198
+ const url = this.emitExpr(stmt.url);
199
+ if (stmt.method === 'GET') {
200
+ this.line(`const _res = await fetch(${url});`);
201
+ }
202
+ else {
203
+ this.line(`const _res = await fetch(${url}, { method: '${stmt.method}' });`);
204
+ }
205
+ this.line(`const ${stmt.resultName} = await _res.json();`);
206
+ if (stmt.onSuccess) {
207
+ this.line(`const ${stmt.onSuccess.param} = ${stmt.resultName};`);
208
+ for (const s of stmt.onSuccess.body) {
209
+ this.emitStatement(s, stateVars);
210
+ }
211
+ }
212
+ this.depth--;
213
+ if (stmt.onFailure) {
214
+ this.line(`} catch (${stmt.onFailure.param}) {`);
215
+ this.depth++;
216
+ for (const s of stmt.onFailure.body) {
217
+ this.emitStatement(s, stateVars);
218
+ }
219
+ this.depth--;
220
+ }
221
+ this.line(`}`);
222
+ break;
223
+ }
224
+ case 'GoToStatement': {
225
+ const passing = stmt.passing ? `, { item: ${this.emitExpr(stmt.passing)} }` : '';
226
+ this.line(`navigation.navigate('${stmt.screen}'${passing});`);
227
+ break;
228
+ }
229
+ case 'PresentStatement':
230
+ this.line(`navigation.navigate('${stmt.screen}');`);
231
+ break;
232
+ case 'GoBackStatement':
233
+ this.line(`navigation.goBack();`);
234
+ break;
235
+ case 'DismissStatement':
236
+ this.line(`navigation.goBack();`);
237
+ break;
238
+ case 'ReturnStatement':
239
+ this.line(`return ${this.emitExpr(stmt.value)};`);
240
+ break;
241
+ case 'CallStatement': {
242
+ const fnName = camelCase(stmt.verbPhrase);
243
+ const args = stmt.args.map(a => this.emitExpr(a)).join(', ');
244
+ this.line(`${fnName}(${args});`);
245
+ break;
246
+ }
247
+ case 'IfStatement': {
248
+ const cond = this.emitConditionJS(stmt.condition);
249
+ this.line(`if (${cond}) {`);
250
+ this.depth++;
251
+ for (const s of stmt.thenBody)
252
+ this.emitStatement(s, stateVars);
253
+ this.depth--;
254
+ for (const branch of stmt.otherwiseIfBranches) {
255
+ this.line(`} else if (${this.emitConditionJS(branch.condition)}) {`);
256
+ this.depth++;
257
+ for (const s of branch.body)
258
+ this.emitStatement(s, stateVars);
259
+ this.depth--;
260
+ }
261
+ if (stmt.elseBody) {
262
+ this.line(`} else {`);
263
+ this.depth++;
264
+ for (const s of stmt.elseBody)
265
+ this.emitStatement(s, stateVars);
266
+ this.depth--;
267
+ }
268
+ this.line(`}`);
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ // ── Layout nodes → JSX ───────────────────────────────────
274
+ emitLayoutNode(node, handlers, stateVars) {
275
+ switch (node.kind) {
276
+ case 'VerticalStack': {
277
+ const styleAttr = this.stackStyleAttr('column', node.spacing, node.padding);
278
+ this.line(`<View${styleAttr}>`);
279
+ this.depth++;
280
+ for (const child of node.children)
281
+ this.emitLayoutNode(child, handlers, stateVars);
282
+ this.depth--;
283
+ this.line(`</View>`);
284
+ break;
285
+ }
286
+ case 'HorizontalStack': {
287
+ const styleAttr = this.stackStyleAttr('row', node.spacing, null);
288
+ this.line(`<View${styleAttr}>`);
289
+ this.depth++;
290
+ for (const child of node.children)
291
+ this.emitLayoutNode(child, handlers, stateVars);
292
+ this.depth--;
293
+ this.line(`</View>`);
294
+ break;
295
+ }
296
+ case 'ScrollView': {
297
+ const horizontal = node.direction === 'horizontal' ? ' horizontal' : '';
298
+ this.line(`<ScrollView${horizontal}>`);
299
+ this.depth++;
300
+ for (const child of node.children)
301
+ this.emitLayoutNode(child, handlers, stateVars);
302
+ this.depth--;
303
+ this.line(`</ScrollView>`);
304
+ break;
305
+ }
306
+ case 'Grid': {
307
+ // Use FlatList with numColumns for grids
308
+ this.line(`{/* grid ${node.columns} columns */}`);
309
+ this.line(`<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: ${node.spacing ?? 0} }}>`);
310
+ this.depth++;
311
+ for (const child of node.children)
312
+ this.emitLayoutNode(child, handlers, stateVars);
313
+ this.depth--;
314
+ this.line(`</View>`);
315
+ break;
316
+ }
317
+ case 'Card': {
318
+ const radius = node.cornerRadius ?? 8;
319
+ this.line(`<View style={[styles.card, { borderRadius: ${radius} }]}>`);
320
+ this.depth++;
321
+ for (const child of node.children)
322
+ this.emitLayoutNode(child, handlers, stateVars);
323
+ this.depth--;
324
+ this.line(`</View>`);
325
+ break;
326
+ }
327
+ case 'Text': {
328
+ const styleRef = this.textStyleRef(node.style, node.color);
329
+ const content = this.exprToJSX(node.expr, stateVars);
330
+ this.line(`<Text${styleRef}>${content}</Text>`);
331
+ break;
332
+ }
333
+ case 'Button': {
334
+ const handler = handlers.get(node.label);
335
+ const onPress = handler ? ` onPress={${handler}}` : ' onPress={() => {}}';
336
+ this.line(`<TouchableOpacity style={styles.button}${onPress}>`);
337
+ this.depth++;
338
+ this.line(`<Text style={styles.buttonText}>${node.label}</Text>`);
339
+ this.depth--;
340
+ this.line(`</TouchableOpacity>`);
341
+ break;
342
+ }
343
+ case 'TextField': {
344
+ const setter = setterName(node.boundTo);
345
+ const placeholder = node.placeholder ? ` placeholder="${node.placeholder}"` : '';
346
+ this.line(`<TextInput`);
347
+ this.depth++;
348
+ this.line(`style={styles.input}`);
349
+ this.line(`value={${node.boundTo}}`);
350
+ this.line(`onChangeText={${setter}}`);
351
+ if (node.placeholder)
352
+ this.line(`placeholder="${node.placeholder}"`);
353
+ this.depth--;
354
+ this.line(`/>`);
355
+ break;
356
+ }
357
+ case 'Image': {
358
+ const src = this.emitExpr(node.source);
359
+ const w = node.width ? node.width : 'undefined';
360
+ const h = node.height ? node.height : 'undefined';
361
+ this.line(`<Image source={{ uri: ${src} }} style={{ width: ${w}, height: ${h} }} />`);
362
+ break;
363
+ }
364
+ case 'Icon':
365
+ this.line(`{/* Icon: ${node.name} */}`);
366
+ break;
367
+ case 'Spacer':
368
+ this.line(`<View style={{ flex: 1 }} />`);
369
+ break;
370
+ case 'Divider':
371
+ this.line(`<View style={styles.divider} />`);
372
+ break;
373
+ case 'LoadingSpinner': {
374
+ const centered = node.centered ? ` style={{ alignSelf: 'center' }}` : '';
375
+ this.line(`<ActivityIndicator${centered} />`);
376
+ break;
377
+ }
378
+ case 'ForEach': {
379
+ const col = this.emitExpr(node.collection);
380
+ this.line(`{${col}.map((${node.item}, _idx) => (`);
381
+ this.depth++;
382
+ this.line(`<React.Fragment key={_idx}>`);
383
+ this.depth++;
384
+ for (const child of node.children)
385
+ this.emitLayoutNode(child, handlers, stateVars);
386
+ this.depth--;
387
+ this.line(`</React.Fragment>`);
388
+ this.depth--;
389
+ this.line(`))}`);
390
+ break;
391
+ }
392
+ case 'LayoutIf': {
393
+ const cond = this.emitConditionJSX(node.condition);
394
+ if (node.otherwiseIfBranches.length === 0 && !node.elseBranch) {
395
+ // Simple && short-circuit
396
+ this.line(`{${cond} && (`);
397
+ this.depth++;
398
+ if (node.thenBranch.length === 1) {
399
+ this.emitLayoutNode(node.thenBranch[0], handlers, stateVars);
400
+ }
401
+ else {
402
+ this.line(`<>`);
403
+ this.depth++;
404
+ for (const child of node.thenBranch)
405
+ this.emitLayoutNode(child, handlers, stateVars);
406
+ this.depth--;
407
+ this.line(`</>`);
408
+ }
409
+ this.depth--;
410
+ this.line(`)}`);
411
+ }
412
+ else {
413
+ // Ternary / nested ternary
414
+ this.line(`{${cond} ? (`);
415
+ this.depth++;
416
+ this.line(`<>`);
417
+ this.depth++;
418
+ for (const child of node.thenBranch)
419
+ this.emitLayoutNode(child, handlers, stateVars);
420
+ this.depth--;
421
+ this.line(`</>`);
422
+ this.depth--;
423
+ this.line(`) : (`);
424
+ this.depth++;
425
+ if (node.elseBranch) {
426
+ this.line(`<>`);
427
+ this.depth++;
428
+ for (const child of node.elseBranch)
429
+ this.emitLayoutNode(child, handlers, stateVars);
430
+ this.depth--;
431
+ this.line(`</>`);
432
+ }
433
+ else {
434
+ this.line(`null`);
435
+ }
436
+ this.depth--;
437
+ this.line(`)}`);
438
+ }
439
+ break;
440
+ }
441
+ case 'ComponentCall': {
442
+ const props = node.args.map(a => `${a.name}={${this.emitExpr(a.value)}}`).join(' ');
443
+ this.line(`<${node.name}${props ? ' ' + props : ''} />`);
444
+ break;
445
+ }
446
+ }
447
+ }
448
+ // ── Expressions → JS strings ──────────────────────────────
449
+ emitExpr(expr) {
450
+ switch (expr.kind) {
451
+ case 'StringLiteral': {
452
+ // Convert {variable} and {object's property} to template literal interpolation
453
+ const hasInterp = /{[\w]/.test(expr.value);
454
+ if (hasInterp) {
455
+ const interpolated = expr.value
456
+ .replace(/{(\w+)'s\s+(\w+)}/g, '${$1.$2}') // {product's name} → ${product.name}
457
+ .replace(/{([\w]+)}/g, '${$1}'); // {count} → ${count}
458
+ return '`' + interpolated + '`';
459
+ }
460
+ return `"${expr.value}"`;
461
+ }
462
+ case 'NumberLiteral': return String(expr.value);
463
+ case 'BooleanLiteral': return String(expr.value);
464
+ case 'NullLiteral': return 'null';
465
+ case 'EmptyListLiteral': return '[]';
466
+ case 'VariableRef': return expr.name;
467
+ case 'PropertyAccess': return `${expr.object}.${expr.property}`;
468
+ case 'BinaryExpr': return `(${this.emitExpr(expr.left)} ${expr.op} ${this.emitExpr(expr.right)})`;
469
+ case 'UnaryExpr': return `${expr.op}(${this.emitExpr(expr.operand)})`;
470
+ }
471
+ }
472
+ // Expression inside JSX curly braces — may need String() for numbers
473
+ exprToJSX(expr, stateVars) {
474
+ if (expr.kind === 'StringLiteral') {
475
+ // Interpolated string: render as JSX expression with template literal
476
+ if (/{[\w]/.test(expr.value)) {
477
+ const interpolated = expr.value
478
+ .replace(/{(\w+)'s\s+(\w+)}/g, '${$1.$2}')
479
+ .replace(/{([\w]+)}/g, '${$1}');
480
+ return '{`' + interpolated + '`}';
481
+ }
482
+ return expr.value;
483
+ }
484
+ const js = this.emitExpr(expr);
485
+ if (expr.kind === 'VariableRef' && stateVars.get(expr.name) === 'number') {
486
+ return `{String(${js})}`;
487
+ }
488
+ return `{${js}}`;
489
+ }
490
+ // ── Conditions → JS boolean expressions ──────────────────
491
+ emitConditionJS(cond) {
492
+ switch (cond.kind) {
493
+ case 'Truthy': return this.emitExpr(cond.expr);
494
+ case 'Falsy': return `!${this.emitExpr(cond.expr)}`;
495
+ case 'Equals': return `${this.emitExpr(cond.left)} === ${this.emitExpr(cond.right)}`;
496
+ case 'NotEquals': return `${this.emitExpr(cond.left)} !== ${this.emitExpr(cond.right)}`;
497
+ case 'GreaterThan': return `${this.emitExpr(cond.left)} > ${this.emitExpr(cond.right)}`;
498
+ case 'LessThan': return `${this.emitExpr(cond.left)} < ${this.emitExpr(cond.right)}`;
499
+ case 'AtLeast': return `${this.emitExpr(cond.left)} >= ${this.emitExpr(cond.right)}`;
500
+ case 'AtMost': return `${this.emitExpr(cond.left)} <= ${this.emitExpr(cond.right)}`;
501
+ case 'HasItems': return `${this.emitExpr(cond.expr)}.length > 0`;
502
+ case 'IsEmpty': return `${this.emitExpr(cond.expr)}.length === 0`;
503
+ case 'Exists': return `${this.emitExpr(cond.expr)} != null`;
504
+ case 'Contains': return `${this.emitExpr(cond.haystack)}.includes(${this.emitExpr(cond.needle)})`;
505
+ case 'And': return `(${this.emitConditionJS(cond.left)}) && (${this.emitConditionJS(cond.right)})`;
506
+ case 'Or': return `(${this.emitConditionJS(cond.left)}) || (${this.emitConditionJS(cond.right)})`;
507
+ case 'Not': return `!(${this.emitConditionJS(cond.inner)})`;
508
+ }
509
+ }
510
+ // Condition suitable for JSX interpolation {cond && ... }
511
+ emitConditionJSX(cond) {
512
+ return this.emitConditionJS(cond);
513
+ }
514
+ // ── StyleSheet ────────────────────────────────────────────
515
+ emitStyleSheet() {
516
+ this.line(`const styles = StyleSheet.create({`);
517
+ this.depth++;
518
+ this.line(`container: { flex: 1, backgroundColor: '#fff' },`);
519
+ this.line(`inner: { flex: 1, padding: 16 },`);
520
+ this.line(`heading: { fontSize: 24, fontWeight: 'bold', color: '#111' },`);
521
+ this.line(`subheading: { fontSize: 18, fontWeight: '600', color: '#222' },`);
522
+ this.line(`body: { fontSize: 16, color: '#333' },`);
523
+ this.line(`caption: { fontSize: 12, color: '#888' },`);
524
+ this.line(`button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 14, alignItems: 'center' },`);
525
+ this.line(`buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },`);
526
+ this.line(`input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, fontSize: 16 },`);
527
+ this.line(`card: { backgroundColor: '#fff', borderRadius: 8, padding: 16, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4, elevation: 3 },`);
528
+ this.line(`divider: { height: 1, backgroundColor: '#eee', marginVertical: 8 },`);
529
+ this.depth--;
530
+ this.line(`});`);
531
+ }
532
+ // ── Helpers ────────────────────────────────────────────────
533
+ inferStateTypes(fields) {
534
+ const map = new Map();
535
+ for (const f of fields) {
536
+ if (f.typeHint) {
537
+ map.set(f.name, this.tsTypeFromAnnotation(f.typeHint));
538
+ }
539
+ else {
540
+ switch (f.defaultValue.kind) {
541
+ case 'StringLiteral':
542
+ map.set(f.name, 'string');
543
+ break;
544
+ case 'NumberLiteral':
545
+ map.set(f.name, 'number');
546
+ break;
547
+ case 'BooleanLiteral':
548
+ map.set(f.name, 'boolean');
549
+ break;
550
+ default:
551
+ map.set(f.name, 'any');
552
+ break;
553
+ }
554
+ }
555
+ }
556
+ return map;
557
+ }
558
+ tsType(field, stateVarTypes) {
559
+ return stateVarTypes.get(field.name) ?? 'any';
560
+ }
561
+ tsTypeFromAnnotation(ann) {
562
+ switch (ann.kind) {
563
+ case 'SimpleType': return this.mapSimpleType(ann.name);
564
+ case 'ListType': return `${this.mapSimpleType(ann.itemType)}[]`;
565
+ case 'OptionalType': return `${this.tsTypeFromAnnotation(ann.inner)} | null`;
566
+ }
567
+ }
568
+ mapSimpleType(name) {
569
+ const map = {
570
+ text: 'string', number: 'number', numeric: 'number', decimal: 'number',
571
+ boolean: 'boolean', date: 'Date', any: 'any'
572
+ };
573
+ return map[name] ?? name;
574
+ }
575
+ buildButtonHandlerMap(events) {
576
+ const map = new Map();
577
+ for (const e of events) {
578
+ if (e.trigger.kind === 'ButtonPressed') {
579
+ map.set(e.trigger.label, buttonHandlerName(e.trigger.label));
580
+ }
581
+ }
582
+ return map;
583
+ }
584
+ collectRNImports(layout, events) {
585
+ const needed = new Set(['View', 'StyleSheet']);
586
+ const scan = (nodes) => {
587
+ for (const node of nodes) {
588
+ switch (node.kind) {
589
+ case 'Text':
590
+ needed.add('Text');
591
+ break;
592
+ case 'Button':
593
+ needed.add('TouchableOpacity');
594
+ needed.add('Text');
595
+ break;
596
+ case 'TextField':
597
+ needed.add('TextInput');
598
+ break;
599
+ case 'Image':
600
+ needed.add('Image');
601
+ break;
602
+ case 'LoadingSpinner':
603
+ needed.add('ActivityIndicator');
604
+ break;
605
+ case 'ScrollView':
606
+ needed.add('ScrollView');
607
+ break;
608
+ }
609
+ if ('children' in node)
610
+ scan(node.children);
611
+ if ('thenBranch' in node) {
612
+ scan(node.thenBranch);
613
+ if (node.elseBranch)
614
+ scan(node.elseBranch);
615
+ }
616
+ if ('body' in node && Array.isArray(node.body))
617
+ scan(node.body);
618
+ }
619
+ };
620
+ scan(layout);
621
+ return Array.from(needed).sort();
622
+ }
623
+ textStyleRef(style, color) {
624
+ const styleStr = `styles.${style}`;
625
+ if (!color)
626
+ return ` style={${styleStr}}`;
627
+ return ` style={[${styleStr}, { color: '${color}' }]}`;
628
+ }
629
+ stackStyleAttr(direction, spacing, padding) {
630
+ const parts = [`flexDirection: '${direction}'`];
631
+ if (spacing)
632
+ parts.push(`gap: ${spacing}`);
633
+ if (padding)
634
+ parts.push(`padding: ${padding}`);
635
+ return ` style={{ ${parts.join(', ')} }}`;
636
+ }
637
+ // ── Output helpers ────────────────────────────────────────
638
+ line(text) {
639
+ const pad = ' '.repeat(this.depth);
640
+ this.out.push(pad + text);
641
+ }
642
+ }
643
+ exports.ReactNativeBackend = ReactNativeBackend;
644
+ // ─────────────────────────────────────────────────────────────
645
+ // Utility functions
646
+ // ─────────────────────────────────────────────────────────────
647
+ function setterName(name) {
648
+ return 'set' + name[0].toUpperCase() + name.slice(1);
649
+ }
650
+ function camelCase(words) {
651
+ return words
652
+ .map((w, i) => i === 0 ? w : w[0].toUpperCase() + w.slice(1))
653
+ .join('');
654
+ }
655
+ function buttonHandlerName(label) {
656
+ return 'handle' + label.split(/\s+/).map(w => w[0].toUpperCase() + w.slice(1)).join('');
657
+ }