english-lang 0.2.3 → 0.2.5
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/src/codegen/ReactNativeBackend.js +185 -32
- package/dist/src/parser/Parser.js +331 -50
- package/package.json +1 -1
|
@@ -60,14 +60,24 @@ class ReactNativeBackend {
|
|
|
60
60
|
if (rnImports.length) {
|
|
61
61
|
this.line(`import { ${rnImports.join(', ')} } from 'react-native';`);
|
|
62
62
|
}
|
|
63
|
+
if (this.hasSlider(screen.layout)) {
|
|
64
|
+
this.line(`import Slider from '@react-native-community/slider';`);
|
|
65
|
+
}
|
|
63
66
|
this.line('');
|
|
64
67
|
// Type interfaces (noun declarations)
|
|
65
68
|
for (const type of types) {
|
|
66
69
|
this.emitTypeDecl(type);
|
|
67
70
|
}
|
|
68
71
|
// Component function
|
|
69
|
-
|
|
72
|
+
const hasRouteParams = screen.receives && screen.receives.length > 0;
|
|
73
|
+
const propsArg = hasRouteParams ? '{ navigation, route }' : '{ navigation }';
|
|
74
|
+
this.line(`export function ${screen.name}(${propsArg}: any) {`);
|
|
70
75
|
this.depth++;
|
|
76
|
+
// Destructure navigation params
|
|
77
|
+
if (hasRouteParams) {
|
|
78
|
+
this.line(`const { ${screen.receives.join(', ')} } = (route.params ?? {}) as any;`);
|
|
79
|
+
this.line('');
|
|
80
|
+
}
|
|
71
81
|
// useState hooks
|
|
72
82
|
for (const field of screen.state) {
|
|
73
83
|
const tsType = this.tsType(field, stateVarTypes);
|
|
@@ -80,10 +90,22 @@ class ReactNativeBackend {
|
|
|
80
90
|
// useEffect for screen opens
|
|
81
91
|
const screenOpensHandler = screen.events.find(e => e.trigger.kind === 'ScreenOpens');
|
|
82
92
|
if (screenOpensHandler) {
|
|
93
|
+
const isAsync = this.bodyIsAsync(screenOpensHandler.body);
|
|
83
94
|
this.line('useEffect(() => {');
|
|
84
95
|
this.depth++;
|
|
85
|
-
|
|
86
|
-
this.
|
|
96
|
+
if (isAsync) {
|
|
97
|
+
this.line('(async () => {');
|
|
98
|
+
this.depth++;
|
|
99
|
+
for (const stmt of screenOpensHandler.body) {
|
|
100
|
+
this.emitStatement(stmt, stateVarTypes);
|
|
101
|
+
}
|
|
102
|
+
this.depth--;
|
|
103
|
+
this.line('})();');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
for (const stmt of screenOpensHandler.body) {
|
|
107
|
+
this.emitStatement(stmt, stateVarTypes);
|
|
108
|
+
}
|
|
87
109
|
}
|
|
88
110
|
this.depth--;
|
|
89
111
|
this.line('}, []);');
|
|
@@ -94,7 +116,8 @@ class ReactNativeBackend {
|
|
|
94
116
|
if (event.trigger.kind !== 'ButtonPressed')
|
|
95
117
|
continue;
|
|
96
118
|
const fnName = buttonHandlers.get(event.trigger.label);
|
|
97
|
-
|
|
119
|
+
const asyncKw = this.bodyIsAsync(event.body) ? 'async ' : '';
|
|
120
|
+
this.line(`const ${fnName} = ${asyncKw}() => {`);
|
|
98
121
|
this.depth++;
|
|
99
122
|
for (const stmt of event.body) {
|
|
100
123
|
this.emitStatement(stmt, stateVarTypes);
|
|
@@ -218,6 +241,10 @@ class ReactNativeBackend {
|
|
|
218
241
|
}
|
|
219
242
|
this.depth--;
|
|
220
243
|
}
|
|
244
|
+
else {
|
|
245
|
+
this.line(`} catch (_e) {`);
|
|
246
|
+
this.line(` // network error — add "on failure: (err) ->" to handle it`);
|
|
247
|
+
}
|
|
221
248
|
this.line(`}`);
|
|
222
249
|
break;
|
|
223
250
|
}
|
|
@@ -274,7 +301,7 @@ class ReactNativeBackend {
|
|
|
274
301
|
emitLayoutNode(node, handlers, stateVars) {
|
|
275
302
|
switch (node.kind) {
|
|
276
303
|
case 'VerticalStack': {
|
|
277
|
-
const styleAttr = this.
|
|
304
|
+
const styleAttr = this.buildStackStyle('column', node);
|
|
278
305
|
this.line(`<View${styleAttr}>`);
|
|
279
306
|
this.depth++;
|
|
280
307
|
for (const child of node.children)
|
|
@@ -284,7 +311,7 @@ class ReactNativeBackend {
|
|
|
284
311
|
break;
|
|
285
312
|
}
|
|
286
313
|
case 'HorizontalStack': {
|
|
287
|
-
const styleAttr = this.
|
|
314
|
+
const styleAttr = this.buildStackStyle('row', node);
|
|
288
315
|
this.line(`<View${styleAttr}>`);
|
|
289
316
|
this.depth++;
|
|
290
317
|
for (const child of node.children)
|
|
@@ -295,7 +322,8 @@ class ReactNativeBackend {
|
|
|
295
322
|
}
|
|
296
323
|
case 'ScrollView': {
|
|
297
324
|
const horizontal = node.direction === 'horizontal' ? ' horizontal' : '';
|
|
298
|
-
|
|
325
|
+
const svColor = node.color ? ` style={{ backgroundColor: '${node.color}' }}` : '';
|
|
326
|
+
this.line(`<ScrollView${horizontal}${svColor}>`);
|
|
299
327
|
this.depth++;
|
|
300
328
|
for (const child of node.children)
|
|
301
329
|
this.emitLayoutNode(child, handlers, stateVars);
|
|
@@ -304,9 +332,12 @@ class ReactNativeBackend {
|
|
|
304
332
|
break;
|
|
305
333
|
}
|
|
306
334
|
case 'Grid': {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
335
|
+
const gridParts = [`flexDirection: 'row'`, 'flexWrap: \'wrap\''];
|
|
336
|
+
if (node.spacing)
|
|
337
|
+
gridParts.push(`gap: ${node.spacing}`);
|
|
338
|
+
if (node.color)
|
|
339
|
+
gridParts.push(`backgroundColor: '${node.color}'`);
|
|
340
|
+
this.line(`<View style={{ ${gridParts.join(', ')} }}>`);
|
|
310
341
|
this.depth++;
|
|
311
342
|
for (const child of node.children)
|
|
312
343
|
this.emitLayoutNode(child, handlers, stateVars);
|
|
@@ -315,8 +346,26 @@ class ReactNativeBackend {
|
|
|
315
346
|
break;
|
|
316
347
|
}
|
|
317
348
|
case 'Card': {
|
|
318
|
-
const
|
|
319
|
-
|
|
349
|
+
const cardParts = [];
|
|
350
|
+
if (node.cornerRadius != null)
|
|
351
|
+
cardParts.push(`borderRadius: ${node.cornerRadius}`);
|
|
352
|
+
if (node.color)
|
|
353
|
+
cardParts.push(`backgroundColor: '${node.color}'`);
|
|
354
|
+
if (node.borderColor)
|
|
355
|
+
cardParts.push(`borderColor: '${node.borderColor}', borderWidth: 1`);
|
|
356
|
+
const cardStyle = cardParts.length
|
|
357
|
+
? `[styles.card, { ${cardParts.join(', ')} }]`
|
|
358
|
+
: 'styles.card';
|
|
359
|
+
this.line(`<View style={${cardStyle}}>`);
|
|
360
|
+
this.depth++;
|
|
361
|
+
for (const child of node.children)
|
|
362
|
+
this.emitLayoutNode(child, handlers, stateVars);
|
|
363
|
+
this.depth--;
|
|
364
|
+
this.line(`</View>`);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'Overlay': {
|
|
368
|
+
this.line(`<View style={{ position: 'relative' }}>`);
|
|
320
369
|
this.depth++;
|
|
321
370
|
for (const child of node.children)
|
|
322
371
|
this.emitLayoutNode(child, handlers, stateVars);
|
|
@@ -325,7 +374,7 @@ class ReactNativeBackend {
|
|
|
325
374
|
break;
|
|
326
375
|
}
|
|
327
376
|
case 'Text': {
|
|
328
|
-
const styleRef = this.textStyleRef(node
|
|
377
|
+
const styleRef = this.textStyleRef(node);
|
|
329
378
|
const content = this.exprToJSX(node.expr, stateVars);
|
|
330
379
|
this.line(`<Text${styleRef}>${content}</Text>`);
|
|
331
380
|
break;
|
|
@@ -333,16 +382,25 @@ class ReactNativeBackend {
|
|
|
333
382
|
case 'Button': {
|
|
334
383
|
const handler = handlers.get(node.label);
|
|
335
384
|
const onPress = handler ? ` onPress={${handler}}` : ' onPress={() => {}}';
|
|
336
|
-
|
|
385
|
+
const disabled = node.disabled ? ' disabled' : '';
|
|
386
|
+
const btnParts = [];
|
|
387
|
+
if (node.outline) {
|
|
388
|
+
btnParts.push(`borderWidth: 1`, `borderColor: '${node.color ?? '#007AFF'}'`, `backgroundColor: 'transparent'`);
|
|
389
|
+
}
|
|
390
|
+
else if (node.color) {
|
|
391
|
+
btnParts.push(`backgroundColor: '${node.color}'`);
|
|
392
|
+
}
|
|
393
|
+
const btnStyle = btnParts.length ? `[styles.button, { ${btnParts.join(', ')} }]` : 'styles.button';
|
|
394
|
+
const txtStyle = node.textColor ? `[styles.buttonText, { color: '${node.textColor}' }]` : 'styles.buttonText';
|
|
395
|
+
this.line(`<TouchableOpacity style={${btnStyle}}${onPress}${disabled}>`);
|
|
337
396
|
this.depth++;
|
|
338
|
-
this.line(`<Text style={
|
|
397
|
+
this.line(`<Text style={${txtStyle}}>${node.label}</Text>`);
|
|
339
398
|
this.depth--;
|
|
340
399
|
this.line(`</TouchableOpacity>`);
|
|
341
400
|
break;
|
|
342
401
|
}
|
|
343
402
|
case 'TextField': {
|
|
344
403
|
const setter = setterName(node.boundTo);
|
|
345
|
-
const placeholder = node.placeholder ? ` placeholder="${node.placeholder}"` : '';
|
|
346
404
|
this.line(`<TextInput`);
|
|
347
405
|
this.depth++;
|
|
348
406
|
this.line(`style={styles.input}`);
|
|
@@ -350,20 +408,38 @@ class ReactNativeBackend {
|
|
|
350
408
|
this.line(`onChangeText={${setter}}`);
|
|
351
409
|
if (node.placeholder)
|
|
352
410
|
this.line(`placeholder="${node.placeholder}"`);
|
|
411
|
+
if (node.multiline)
|
|
412
|
+
this.line(`multiline`);
|
|
413
|
+
if (node.secure)
|
|
414
|
+
this.line(`secureTextEntry`);
|
|
415
|
+
if (node.keyboard)
|
|
416
|
+
this.line(`keyboardType="${node.keyboard}"`);
|
|
353
417
|
this.depth--;
|
|
354
418
|
this.line(`/>`);
|
|
355
419
|
break;
|
|
356
420
|
}
|
|
357
421
|
case 'Image': {
|
|
358
422
|
const src = this.emitExpr(node.source);
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
423
|
+
const imgParts = [];
|
|
424
|
+
if (node.width)
|
|
425
|
+
imgParts.push(`width: ${node.width}`);
|
|
426
|
+
if (node.height)
|
|
427
|
+
imgParts.push(`height: ${node.height}`);
|
|
428
|
+
if (node.rounded) {
|
|
429
|
+
const r = Math.min(node.width ?? 50, node.height ?? 50) / 2;
|
|
430
|
+
imgParts.push(`borderRadius: ${r}`);
|
|
431
|
+
}
|
|
432
|
+
const imgStyle = imgParts.length ? `{ ${imgParts.join(', ')} }` : '{}';
|
|
433
|
+
this.line(`<Image source={{ uri: ${src} }} style={${imgStyle}} />`);
|
|
362
434
|
break;
|
|
363
435
|
}
|
|
364
|
-
case 'Icon':
|
|
365
|
-
|
|
436
|
+
case 'Icon': {
|
|
437
|
+
const size = node.size ?? 24;
|
|
438
|
+
const color = node.color ? ` color="${node.color}"` : '';
|
|
439
|
+
this.line(`{/* Icon: ${node.name} — install react-native-vector-icons */}`);
|
|
440
|
+
this.line(`{/* <Icon name="${node.name}" size={${size}}${color} /> */}`);
|
|
366
441
|
break;
|
|
442
|
+
}
|
|
367
443
|
case 'Spacer':
|
|
368
444
|
this.line(`<View style={{ flex: 1 }} />`);
|
|
369
445
|
break;
|
|
@@ -372,7 +448,30 @@ class ReactNativeBackend {
|
|
|
372
448
|
break;
|
|
373
449
|
case 'LoadingSpinner': {
|
|
374
450
|
const centered = node.centered ? ` style={{ alignSelf: 'center' }}` : '';
|
|
375
|
-
|
|
451
|
+
const color = node.color ? ` color="${node.color}"` : '';
|
|
452
|
+
this.line(`<ActivityIndicator${centered}${color} />`);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'Switch': {
|
|
456
|
+
const setter = setterName(node.boundTo);
|
|
457
|
+
const trackColor = node.color
|
|
458
|
+
? ` trackColor={{ false: '#767577', true: '${node.color}' }}`
|
|
459
|
+
: '';
|
|
460
|
+
this.line(`<Switch value={${node.boundTo}} onValueChange={${setter}}${trackColor} />`);
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case 'Slider': {
|
|
464
|
+
const setter = setterName(node.boundTo);
|
|
465
|
+
this.line(`<Slider`);
|
|
466
|
+
this.depth++;
|
|
467
|
+
this.line(`minimumValue={${node.min}}`);
|
|
468
|
+
this.line(`maximumValue={${node.max}}`);
|
|
469
|
+
if (node.step != null)
|
|
470
|
+
this.line(`step={${node.step}}`);
|
|
471
|
+
this.line(`value={${node.boundTo}}`);
|
|
472
|
+
this.line(`onValueChange={${setter}}`);
|
|
473
|
+
this.depth--;
|
|
474
|
+
this.line(`/>`);
|
|
376
475
|
break;
|
|
377
476
|
}
|
|
378
477
|
case 'ForEach': {
|
|
@@ -572,6 +671,36 @@ class ReactNativeBackend {
|
|
|
572
671
|
};
|
|
573
672
|
return map[name] ?? name;
|
|
574
673
|
}
|
|
674
|
+
bodyIsAsync(stmts) {
|
|
675
|
+
return stmts.some(s => {
|
|
676
|
+
if (s.kind === 'FetchStatement')
|
|
677
|
+
return true;
|
|
678
|
+
if (s.kind === 'IfStatement') {
|
|
679
|
+
return this.bodyIsAsync(s.thenBody)
|
|
680
|
+
|| s.otherwiseIfBranches.some(b => this.bodyIsAsync(b.body))
|
|
681
|
+
|| (s.elseBody ? this.bodyIsAsync(s.elseBody) : false);
|
|
682
|
+
}
|
|
683
|
+
return false;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
hasSlider(layout) {
|
|
687
|
+
const scan = (nodes) => {
|
|
688
|
+
for (const node of nodes) {
|
|
689
|
+
if (node.kind === 'Slider')
|
|
690
|
+
return true;
|
|
691
|
+
if ('children' in node && scan(node.children))
|
|
692
|
+
return true;
|
|
693
|
+
if ('thenBranch' in node) {
|
|
694
|
+
if (scan(node.thenBranch))
|
|
695
|
+
return true;
|
|
696
|
+
if (node.elseBranch && scan(node.elseBranch))
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return false;
|
|
701
|
+
};
|
|
702
|
+
return scan(layout);
|
|
703
|
+
}
|
|
575
704
|
buildButtonHandlerMap(events) {
|
|
576
705
|
const map = new Map();
|
|
577
706
|
for (const e of events) {
|
|
@@ -605,6 +734,10 @@ class ReactNativeBackend {
|
|
|
605
734
|
case 'ScrollView':
|
|
606
735
|
needed.add('ScrollView');
|
|
607
736
|
break;
|
|
737
|
+
case 'Switch':
|
|
738
|
+
needed.add('Switch');
|
|
739
|
+
break;
|
|
740
|
+
// Slider comes from @react-native-community/slider, imported separately
|
|
608
741
|
}
|
|
609
742
|
if ('children' in node)
|
|
610
743
|
scan(node.children);
|
|
@@ -620,18 +753,38 @@ class ReactNativeBackend {
|
|
|
620
753
|
scan(layout);
|
|
621
754
|
return Array.from(needed).sort();
|
|
622
755
|
}
|
|
623
|
-
textStyleRef(
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
756
|
+
textStyleRef(node) {
|
|
757
|
+
const inline = [];
|
|
758
|
+
if (node.color)
|
|
759
|
+
inline.push(`color: '${node.color}'`);
|
|
760
|
+
if (node.size)
|
|
761
|
+
inline.push(`fontSize: ${node.size}`);
|
|
762
|
+
if (node.bold)
|
|
763
|
+
inline.push(`fontWeight: 'bold' as const`);
|
|
764
|
+
if (node.italic)
|
|
765
|
+
inline.push(`fontStyle: 'italic' as const`);
|
|
766
|
+
if (inline.length === 0)
|
|
767
|
+
return ` style={styles.${node.style}}`;
|
|
768
|
+
return ` style={[styles.${node.style}, { ${inline.join(', ')} }]}`;
|
|
628
769
|
}
|
|
629
|
-
|
|
770
|
+
buildStackStyle(direction, node) {
|
|
630
771
|
const parts = [`flexDirection: '${direction}'`];
|
|
631
|
-
if (spacing)
|
|
632
|
-
parts.push(`gap: ${spacing}`);
|
|
633
|
-
if (padding)
|
|
634
|
-
parts.push(`padding: ${padding}`);
|
|
772
|
+
if (node.spacing)
|
|
773
|
+
parts.push(`gap: ${node.spacing}`);
|
|
774
|
+
if (node.padding)
|
|
775
|
+
parts.push(`padding: ${node.padding}`);
|
|
776
|
+
if (node.color)
|
|
777
|
+
parts.push(`backgroundColor: '${node.color}'`);
|
|
778
|
+
if (node.borderColor)
|
|
779
|
+
parts.push(`borderColor: '${node.borderColor}', borderWidth: 1`);
|
|
780
|
+
if (node.borderRadius)
|
|
781
|
+
parts.push(`borderRadius: ${node.borderRadius}`);
|
|
782
|
+
if (node.opacity)
|
|
783
|
+
parts.push(`opacity: ${node.opacity}`);
|
|
784
|
+
if (node.align)
|
|
785
|
+
parts.push(`alignItems: '${node.align}'`);
|
|
786
|
+
if (node.justify)
|
|
787
|
+
parts.push(`justifyContent: '${node.justify}'`);
|
|
635
788
|
return ` style={{ ${parts.join(', ')} }}`;
|
|
636
789
|
}
|
|
637
790
|
// ── Output helpers ────────────────────────────────────────
|
|
@@ -55,6 +55,7 @@ class Parser {
|
|
|
55
55
|
this.expect('NEWLINE');
|
|
56
56
|
this.expect('INDENT');
|
|
57
57
|
let title = null;
|
|
58
|
+
const receives = [];
|
|
58
59
|
const state = [];
|
|
59
60
|
const events = [];
|
|
60
61
|
const actions = [];
|
|
@@ -70,6 +71,21 @@ class Parser {
|
|
|
70
71
|
title = this.expect('STRING').value;
|
|
71
72
|
this.expectNewline();
|
|
72
73
|
}
|
|
74
|
+
else if (word === 'receives') {
|
|
75
|
+
// receives: param1, param2 ... (navigation route.params)
|
|
76
|
+
this.consumeWord();
|
|
77
|
+
this.expect('COLON');
|
|
78
|
+
this.expect('NEWLINE');
|
|
79
|
+
this.expect('INDENT');
|
|
80
|
+
while (!this.check('DEDENT') && !this.check('EOF')) {
|
|
81
|
+
this.skipNewlines();
|
|
82
|
+
if (this.check('DEDENT'))
|
|
83
|
+
break;
|
|
84
|
+
receives.push(this.expectWord());
|
|
85
|
+
this.expectNewline();
|
|
86
|
+
}
|
|
87
|
+
this.expect('DEDENT');
|
|
88
|
+
}
|
|
73
89
|
else if (word === 'state') {
|
|
74
90
|
this.consumeWord();
|
|
75
91
|
this.expect('COLON');
|
|
@@ -110,7 +126,7 @@ class Parser {
|
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
this.expect('DEDENT');
|
|
113
|
-
return { kind: 'ScreenDecl', name, title, state, events, actions, layout, line };
|
|
129
|
+
return { kind: 'ScreenDecl', name, title, receives, state, events, actions, layout, line };
|
|
114
130
|
}
|
|
115
131
|
// ── State field ───────────────────────────────────────────
|
|
116
132
|
//
|
|
@@ -663,6 +679,12 @@ class Parser {
|
|
|
663
679
|
}
|
|
664
680
|
if (word === 'loading')
|
|
665
681
|
return this.parseLoadingSpinner();
|
|
682
|
+
if (word === 'switch')
|
|
683
|
+
return this.parseSwitch();
|
|
684
|
+
if (word === 'slider')
|
|
685
|
+
return this.parseSlider();
|
|
686
|
+
if (word === 'overlay')
|
|
687
|
+
return this.parseOverlay();
|
|
666
688
|
if (word === 'if')
|
|
667
689
|
return this.parseLayoutIf();
|
|
668
690
|
if (word === 'for')
|
|
@@ -672,12 +694,18 @@ class Parser {
|
|
|
672
694
|
return this.parseComponentCall();
|
|
673
695
|
throw this.error(`Unknown layout element "${word}"`);
|
|
674
696
|
}
|
|
675
|
-
// vertical stack [spacing N] [padding N]:
|
|
697
|
+
// vertical stack [spacing N] [padding N] [color X] [border color X] [rounded N] [opacity N] [align X] [justify X]:
|
|
676
698
|
parseVerticalStack() {
|
|
677
699
|
this.expectWord('vertical');
|
|
678
700
|
this.expectWord('stack');
|
|
679
701
|
let spacing = null;
|
|
680
702
|
let padding = null;
|
|
703
|
+
let color = null;
|
|
704
|
+
let borderColor = null;
|
|
705
|
+
let borderRadius = null;
|
|
706
|
+
let opacity = null;
|
|
707
|
+
let align = null;
|
|
708
|
+
let justify = null;
|
|
681
709
|
while (this.check('WORD')) {
|
|
682
710
|
const kw = this.peekWord();
|
|
683
711
|
if (kw === 'spacing') {
|
|
@@ -688,6 +716,36 @@ class Parser {
|
|
|
688
716
|
this.consumeWord();
|
|
689
717
|
padding = parseFloat(this.expect('NUMBER').value);
|
|
690
718
|
}
|
|
719
|
+
else if (kw === 'color') {
|
|
720
|
+
this.consumeWord();
|
|
721
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
722
|
+
}
|
|
723
|
+
else if (kw === 'border') {
|
|
724
|
+
this.consumeWord();
|
|
725
|
+
if (this.peekWord() === 'color') {
|
|
726
|
+
this.consumeWord();
|
|
727
|
+
borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
borderRadius = parseFloat(this.expect('NUMBER').value);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else if (kw === 'rounded') {
|
|
734
|
+
this.consumeWord();
|
|
735
|
+
borderRadius = parseFloat(this.expect('NUMBER').value);
|
|
736
|
+
}
|
|
737
|
+
else if (kw === 'opacity') {
|
|
738
|
+
this.consumeWord();
|
|
739
|
+
opacity = parseFloat(this.expect('NUMBER').value);
|
|
740
|
+
}
|
|
741
|
+
else if (kw === 'align') {
|
|
742
|
+
this.consumeWord();
|
|
743
|
+
align = this.expectWord();
|
|
744
|
+
}
|
|
745
|
+
else if (kw === 'justify') {
|
|
746
|
+
this.consumeWord();
|
|
747
|
+
justify = this.expectWord();
|
|
748
|
+
}
|
|
691
749
|
else
|
|
692
750
|
break;
|
|
693
751
|
}
|
|
@@ -696,77 +754,150 @@ class Parser {
|
|
|
696
754
|
this.expect('INDENT');
|
|
697
755
|
const children = this.parseLayoutNodes();
|
|
698
756
|
this.expect('DEDENT');
|
|
699
|
-
return { kind: 'VerticalStack', spacing, padding, children };
|
|
757
|
+
return { kind: 'VerticalStack', spacing, padding, color, borderColor, borderRadius, opacity, align, justify, children };
|
|
700
758
|
}
|
|
701
|
-
// horizontal stack [spacing N]:
|
|
759
|
+
// horizontal stack [spacing N] [padding N] [color X] [border color X] [rounded N] [opacity N] [align X] [justify X]:
|
|
702
760
|
parseHorizontalStack() {
|
|
703
761
|
this.expectWord('horizontal');
|
|
704
762
|
this.expectWord('stack');
|
|
705
763
|
let spacing = null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
764
|
+
let padding = null;
|
|
765
|
+
let color = null;
|
|
766
|
+
let borderColor = null;
|
|
767
|
+
let borderRadius = null;
|
|
768
|
+
let opacity = null;
|
|
769
|
+
let align = null;
|
|
770
|
+
let justify = null;
|
|
771
|
+
while (this.check('WORD')) {
|
|
772
|
+
const kw = this.peekWord();
|
|
773
|
+
if (kw === 'spacing') {
|
|
774
|
+
this.consumeWord();
|
|
775
|
+
spacing = parseFloat(this.expect('NUMBER').value);
|
|
776
|
+
}
|
|
777
|
+
else if (kw === 'padding') {
|
|
778
|
+
this.consumeWord();
|
|
779
|
+
padding = parseFloat(this.expect('NUMBER').value);
|
|
780
|
+
}
|
|
781
|
+
else if (kw === 'color') {
|
|
782
|
+
this.consumeWord();
|
|
783
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
784
|
+
}
|
|
785
|
+
else if (kw === 'border') {
|
|
786
|
+
this.consumeWord();
|
|
787
|
+
if (this.peekWord() === 'color') {
|
|
788
|
+
this.consumeWord();
|
|
789
|
+
borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
borderRadius = parseFloat(this.expect('NUMBER').value);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else if (kw === 'rounded') {
|
|
796
|
+
this.consumeWord();
|
|
797
|
+
borderRadius = parseFloat(this.expect('NUMBER').value);
|
|
798
|
+
}
|
|
799
|
+
else if (kw === 'opacity') {
|
|
800
|
+
this.consumeWord();
|
|
801
|
+
opacity = parseFloat(this.expect('NUMBER').value);
|
|
802
|
+
}
|
|
803
|
+
else if (kw === 'align') {
|
|
804
|
+
this.consumeWord();
|
|
805
|
+
align = this.expectWord();
|
|
806
|
+
}
|
|
807
|
+
else if (kw === 'justify') {
|
|
808
|
+
this.consumeWord();
|
|
809
|
+
justify = this.expectWord();
|
|
810
|
+
}
|
|
811
|
+
else
|
|
812
|
+
break;
|
|
709
813
|
}
|
|
710
814
|
this.expect('COLON');
|
|
711
815
|
this.expect('NEWLINE');
|
|
712
816
|
this.expect('INDENT');
|
|
713
817
|
const children = this.parseLayoutNodes();
|
|
714
818
|
this.expect('DEDENT');
|
|
715
|
-
return { kind: 'HorizontalStack', spacing, children };
|
|
819
|
+
return { kind: 'HorizontalStack', spacing, padding, color, borderColor, borderRadius, opacity, align, justify, children };
|
|
716
820
|
}
|
|
717
|
-
// scroll view [vertical|horizontal]:
|
|
821
|
+
// scroll view [vertical|horizontal] [color X]:
|
|
718
822
|
parseScrollView() {
|
|
719
823
|
this.expectWord('scroll');
|
|
720
824
|
this.expectWord('view');
|
|
721
825
|
let direction = 'vertical';
|
|
826
|
+
let color = null;
|
|
722
827
|
if (this.peekWord() === 'vertical' || this.peekWord() === 'horizontal') {
|
|
723
828
|
direction = this.consumeWord();
|
|
724
829
|
}
|
|
830
|
+
if (this.peekWord() === 'color') {
|
|
831
|
+
this.consumeWord();
|
|
832
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
833
|
+
}
|
|
725
834
|
this.expect('COLON');
|
|
726
835
|
this.expect('NEWLINE');
|
|
727
836
|
this.expect('INDENT');
|
|
728
837
|
const children = this.parseLayoutNodes();
|
|
729
838
|
this.expect('DEDENT');
|
|
730
|
-
return { kind: 'ScrollView', direction, children };
|
|
839
|
+
return { kind: 'ScrollView', direction, color, children };
|
|
731
840
|
}
|
|
732
|
-
// grid with N columns [spacing N]:
|
|
841
|
+
// grid with N columns [spacing N] [color X]:
|
|
733
842
|
parseGrid() {
|
|
734
843
|
this.expectWord('grid');
|
|
735
844
|
this.expectWord('with');
|
|
736
845
|
const columns = parseFloat(this.expect('NUMBER').value);
|
|
737
846
|
this.expectWord('columns');
|
|
738
847
|
let spacing = null;
|
|
848
|
+
let color = null;
|
|
739
849
|
if (this.peekWord() === 'spacing') {
|
|
740
850
|
this.consumeWord();
|
|
741
851
|
spacing = parseFloat(this.expect('NUMBER').value);
|
|
742
852
|
}
|
|
853
|
+
if (this.peekWord() === 'color') {
|
|
854
|
+
this.consumeWord();
|
|
855
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
856
|
+
}
|
|
743
857
|
this.expect('COLON');
|
|
744
858
|
this.expect('NEWLINE');
|
|
745
859
|
this.expect('INDENT');
|
|
746
860
|
const children = this.parseLayoutNodes();
|
|
747
861
|
this.expect('DEDENT');
|
|
748
|
-
return { kind: 'Grid', columns, spacing, children };
|
|
862
|
+
return { kind: 'Grid', columns, spacing, color, children };
|
|
749
863
|
}
|
|
750
|
-
// card [corner radius N]:
|
|
864
|
+
// card [corner radius N] [color X] [border color X]:
|
|
751
865
|
parseCard() {
|
|
752
866
|
this.expectWord('card');
|
|
753
867
|
let cornerRadius = null;
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
868
|
+
let color = null;
|
|
869
|
+
let borderColor = null;
|
|
870
|
+
while (this.check('WORD')) {
|
|
871
|
+
const kw = this.peekWord();
|
|
872
|
+
if (kw === 'corner') {
|
|
873
|
+
this.consumeWord();
|
|
874
|
+
this.expectWord('radius');
|
|
875
|
+
cornerRadius = parseFloat(this.expect('NUMBER').value);
|
|
876
|
+
}
|
|
877
|
+
else if (kw === 'color') {
|
|
878
|
+
this.consumeWord();
|
|
879
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
880
|
+
}
|
|
881
|
+
else if (kw === 'border') {
|
|
882
|
+
this.consumeWord();
|
|
883
|
+
if (this.peekWord() === 'color') {
|
|
884
|
+
this.consumeWord();
|
|
885
|
+
borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
758
891
|
}
|
|
759
892
|
this.expect('COLON');
|
|
760
893
|
this.expect('NEWLINE');
|
|
761
894
|
this.expect('INDENT');
|
|
762
895
|
const children = this.parseLayoutNodes();
|
|
763
896
|
this.expect('DEDENT');
|
|
764
|
-
return { kind: 'Card', cornerRadius, children };
|
|
897
|
+
return { kind: 'Card', cornerRadius, color, borderColor, children };
|
|
765
898
|
}
|
|
766
|
-
// show "text" [as style] [color X]
|
|
767
|
-
// show variable [as style] [color X]
|
|
768
|
-
// text "text" as style
|
|
769
|
-
// text variable as style
|
|
899
|
+
// show "text" [as style] [color X] [size N] [bold] [italic]
|
|
900
|
+
// show variable [as style] [color X] [size N] [bold] [italic]
|
|
770
901
|
parseTextNode() {
|
|
771
902
|
const kw = this.consumeWord(); // 'show' or 'text'
|
|
772
903
|
if (kw !== 'show' && kw !== 'text')
|
|
@@ -787,26 +918,74 @@ class Parser {
|
|
|
787
918
|
style = this.expectWord();
|
|
788
919
|
}
|
|
789
920
|
let color = null;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
921
|
+
let size = null;
|
|
922
|
+
let bold = false;
|
|
923
|
+
let italic = false;
|
|
924
|
+
while (this.check('WORD')) {
|
|
925
|
+
const mod = this.peekWord();
|
|
926
|
+
if (mod === 'color') {
|
|
927
|
+
this.consumeWord();
|
|
928
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
929
|
+
}
|
|
930
|
+
else if (mod === 'size') {
|
|
931
|
+
this.consumeWord();
|
|
932
|
+
size = parseFloat(this.expect('NUMBER').value);
|
|
933
|
+
}
|
|
934
|
+
else if (mod === 'bold') {
|
|
935
|
+
this.consumeWord();
|
|
936
|
+
bold = true;
|
|
937
|
+
}
|
|
938
|
+
else if (mod === 'italic') {
|
|
939
|
+
this.consumeWord();
|
|
940
|
+
italic = true;
|
|
941
|
+
}
|
|
942
|
+
else
|
|
943
|
+
break;
|
|
793
944
|
}
|
|
794
945
|
this.expectNewline();
|
|
795
|
-
return { kind: 'Text', expr, style, color };
|
|
946
|
+
return { kind: 'Text', expr, style, color, size, bold, italic };
|
|
796
947
|
}
|
|
797
|
-
// button "Label" [style
|
|
948
|
+
// button "Label" [style X] [color X] [text color X] [disabled] [outline]
|
|
798
949
|
parseButton() {
|
|
799
950
|
this.expectWord('button');
|
|
800
951
|
const label = this.expect('STRING').value;
|
|
801
952
|
let style = null;
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
953
|
+
let color = null;
|
|
954
|
+
let textColor = null;
|
|
955
|
+
let disabled = false;
|
|
956
|
+
let outline = false;
|
|
957
|
+
while (this.check('WORD')) {
|
|
958
|
+
const kw = this.peekWord();
|
|
959
|
+
if (kw === 'style') {
|
|
960
|
+
this.consumeWord();
|
|
961
|
+
style = this.expectWord();
|
|
962
|
+
}
|
|
963
|
+
else if (kw === 'color') {
|
|
964
|
+
this.consumeWord();
|
|
965
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
966
|
+
}
|
|
967
|
+
else if (kw === 'text') {
|
|
968
|
+
this.consumeWord();
|
|
969
|
+
if (this.peekWord() === 'color') {
|
|
970
|
+
this.consumeWord();
|
|
971
|
+
textColor = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else if (kw === 'disabled') {
|
|
975
|
+
this.consumeWord();
|
|
976
|
+
disabled = true;
|
|
977
|
+
}
|
|
978
|
+
else if (kw === 'outline') {
|
|
979
|
+
this.consumeWord();
|
|
980
|
+
outline = true;
|
|
981
|
+
}
|
|
982
|
+
else
|
|
983
|
+
break;
|
|
805
984
|
}
|
|
806
985
|
this.expectNewline();
|
|
807
|
-
return { kind: 'Button', label, style };
|
|
986
|
+
return { kind: 'Button', label, style, color, textColor, disabled, outline };
|
|
808
987
|
}
|
|
809
|
-
// text field bound to varName [placeholder "..."]
|
|
988
|
+
// text field bound to varName [placeholder "..."] [multiline] [secure] [keyboard X]
|
|
810
989
|
parseTextField() {
|
|
811
990
|
this.expectWord('text');
|
|
812
991
|
this.expectWord('field');
|
|
@@ -814,55 +993,148 @@ class Parser {
|
|
|
814
993
|
this.expectWord('to');
|
|
815
994
|
const boundTo = this.expectWord();
|
|
816
995
|
let placeholder = null;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
996
|
+
let multiline = false;
|
|
997
|
+
let secure = false;
|
|
998
|
+
let keyboard = null;
|
|
999
|
+
while (this.check('WORD')) {
|
|
1000
|
+
const kw = this.peekWord();
|
|
1001
|
+
if (kw === 'placeholder') {
|
|
1002
|
+
this.consumeWord();
|
|
1003
|
+
placeholder = this.expect('STRING').value;
|
|
1004
|
+
}
|
|
1005
|
+
else if (kw === 'multiline') {
|
|
1006
|
+
this.consumeWord();
|
|
1007
|
+
multiline = true;
|
|
1008
|
+
}
|
|
1009
|
+
else if (kw === 'secure') {
|
|
1010
|
+
this.consumeWord();
|
|
1011
|
+
secure = true;
|
|
1012
|
+
}
|
|
1013
|
+
else if (kw === 'keyboard') {
|
|
1014
|
+
this.consumeWord();
|
|
1015
|
+
keyboard = this.expectWord();
|
|
1016
|
+
}
|
|
1017
|
+
else
|
|
1018
|
+
break;
|
|
820
1019
|
}
|
|
821
1020
|
this.expectNewline();
|
|
822
|
-
return { kind: 'TextField', placeholder, boundTo };
|
|
1021
|
+
return { kind: 'TextField', placeholder, boundTo, multiline, secure, keyboard };
|
|
823
1022
|
}
|
|
824
|
-
// image from expr [width N] [height N]
|
|
1023
|
+
// image from expr [width N] [height N] [rounded]
|
|
825
1024
|
parseImage() {
|
|
826
1025
|
this.expectWord('image');
|
|
827
1026
|
this.expectWord('from');
|
|
828
1027
|
const source = this.parseExpr();
|
|
829
1028
|
let width = null;
|
|
830
1029
|
let height = null;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1030
|
+
let rounded = false;
|
|
1031
|
+
while (this.check('WORD')) {
|
|
1032
|
+
const kw = this.peekWord();
|
|
1033
|
+
if (kw === 'width') {
|
|
1034
|
+
this.consumeWord();
|
|
1035
|
+
width = parseFloat(this.expect('NUMBER').value);
|
|
1036
|
+
}
|
|
1037
|
+
else if (kw === 'height') {
|
|
1038
|
+
this.consumeWord();
|
|
1039
|
+
height = parseFloat(this.expect('NUMBER').value);
|
|
1040
|
+
}
|
|
1041
|
+
else if (kw === 'rounded') {
|
|
1042
|
+
this.consumeWord();
|
|
1043
|
+
rounded = true;
|
|
1044
|
+
}
|
|
1045
|
+
else
|
|
1046
|
+
break;
|
|
838
1047
|
}
|
|
839
1048
|
this.expectNewline();
|
|
840
|
-
return { kind: 'Image', source, width, height };
|
|
1049
|
+
return { kind: 'Image', source, width, height, rounded };
|
|
841
1050
|
}
|
|
842
|
-
// icon named "name" [size N]
|
|
1051
|
+
// icon named "name" [size N] [color X]
|
|
843
1052
|
parseIcon() {
|
|
844
1053
|
this.expectWord('icon');
|
|
845
1054
|
this.expectWord('named');
|
|
846
1055
|
const name = this.expect('STRING').value;
|
|
847
1056
|
let size = null;
|
|
1057
|
+
let color = null;
|
|
848
1058
|
if (this.peekWord() === 'size') {
|
|
849
1059
|
this.consumeWord();
|
|
850
1060
|
size = parseFloat(this.expect('NUMBER').value);
|
|
851
1061
|
}
|
|
1062
|
+
if (this.peekWord() === 'color') {
|
|
1063
|
+
this.consumeWord();
|
|
1064
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
1065
|
+
}
|
|
852
1066
|
this.expectNewline();
|
|
853
|
-
return { kind: 'Icon', name, size };
|
|
1067
|
+
return { kind: 'Icon', name, size, color };
|
|
854
1068
|
}
|
|
855
|
-
// loading spinner [centered]
|
|
1069
|
+
// loading spinner [centered] [color X]
|
|
856
1070
|
parseLoadingSpinner() {
|
|
857
1071
|
this.expectWord('loading');
|
|
858
1072
|
this.expectWord('spinner');
|
|
859
1073
|
let centered = false;
|
|
1074
|
+
let color = null;
|
|
860
1075
|
if (this.peekWord() === 'centered') {
|
|
861
1076
|
this.consumeWord();
|
|
862
1077
|
centered = true;
|
|
863
1078
|
}
|
|
1079
|
+
if (this.peekWord() === 'color') {
|
|
1080
|
+
this.consumeWord();
|
|
1081
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
1082
|
+
}
|
|
864
1083
|
this.expectNewline();
|
|
865
|
-
return { kind: 'LoadingSpinner', centered };
|
|
1084
|
+
return { kind: 'LoadingSpinner', centered, color };
|
|
1085
|
+
}
|
|
1086
|
+
// switch bound to varName [color X]
|
|
1087
|
+
parseSwitch() {
|
|
1088
|
+
this.expectWord('switch');
|
|
1089
|
+
this.expectWord('bound');
|
|
1090
|
+
this.expectWord('to');
|
|
1091
|
+
const boundTo = this.expectWord();
|
|
1092
|
+
let color = null;
|
|
1093
|
+
if (this.peekWord() === 'color') {
|
|
1094
|
+
this.consumeWord();
|
|
1095
|
+
color = this.check('STRING') ? this.advance().value : this.expectWord();
|
|
1096
|
+
}
|
|
1097
|
+
this.expectNewline();
|
|
1098
|
+
return { kind: 'Switch', boundTo, color };
|
|
1099
|
+
}
|
|
1100
|
+
// slider bound to varName [min N] [max N] [step N]
|
|
1101
|
+
parseSlider() {
|
|
1102
|
+
this.expectWord('slider');
|
|
1103
|
+
this.expectWord('bound');
|
|
1104
|
+
this.expectWord('to');
|
|
1105
|
+
const boundTo = this.expectWord();
|
|
1106
|
+
let min = 0;
|
|
1107
|
+
let max = 100;
|
|
1108
|
+
let step = null;
|
|
1109
|
+
while (this.check('WORD')) {
|
|
1110
|
+
const kw = this.peekWord();
|
|
1111
|
+
if (kw === 'min') {
|
|
1112
|
+
this.consumeWord();
|
|
1113
|
+
min = parseFloat(this.expect('NUMBER').value);
|
|
1114
|
+
}
|
|
1115
|
+
else if (kw === 'max') {
|
|
1116
|
+
this.consumeWord();
|
|
1117
|
+
max = parseFloat(this.expect('NUMBER').value);
|
|
1118
|
+
}
|
|
1119
|
+
else if (kw === 'step') {
|
|
1120
|
+
this.consumeWord();
|
|
1121
|
+
step = parseFloat(this.expect('NUMBER').value);
|
|
1122
|
+
}
|
|
1123
|
+
else
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
this.expectNewline();
|
|
1127
|
+
return { kind: 'Slider', boundTo, min, max, step };
|
|
1128
|
+
}
|
|
1129
|
+
// overlay:
|
|
1130
|
+
parseOverlay() {
|
|
1131
|
+
this.expectWord('overlay');
|
|
1132
|
+
this.expect('COLON');
|
|
1133
|
+
this.expect('NEWLINE');
|
|
1134
|
+
this.expect('INDENT');
|
|
1135
|
+
const children = this.parseLayoutNodes();
|
|
1136
|
+
this.expect('DEDENT');
|
|
1137
|
+
return { kind: 'Overlay', children };
|
|
866
1138
|
}
|
|
867
1139
|
// if condition: ... [otherwise if condition: ...] [otherwise: ...]
|
|
868
1140
|
parseLayoutIf() {
|
|
@@ -1065,8 +1337,17 @@ class Parser {
|
|
|
1065
1337
|
if (word === 'when') {
|
|
1066
1338
|
events.push(this.parseEventHandler());
|
|
1067
1339
|
}
|
|
1340
|
+
else if (word === 'layout') {
|
|
1341
|
+
// Optional layout: section header (same as screens)
|
|
1342
|
+
this.consumeWord();
|
|
1343
|
+
this.expect('COLON');
|
|
1344
|
+
this.expect('NEWLINE');
|
|
1345
|
+
this.expect('INDENT');
|
|
1346
|
+
layout = this.parseLayoutNodes();
|
|
1347
|
+
this.expect('DEDENT');
|
|
1348
|
+
}
|
|
1068
1349
|
else {
|
|
1069
|
-
//
|
|
1350
|
+
// Layout nodes written directly (without layout: header)
|
|
1070
1351
|
layout = this.parseLayoutNodes();
|
|
1071
1352
|
}
|
|
1072
1353
|
}
|