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.
@@ -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
- this.line(`export function ${screen.name}({ navigation }: any) {`);
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
- for (const stmt of screenOpensHandler.body) {
86
- this.emitStatement(stmt, stateVarTypes);
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
- this.line(`const ${fnName} = () => {`);
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.stackStyleAttr('column', node.spacing, node.padding);
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.stackStyleAttr('row', node.spacing, null);
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
- this.line(`<ScrollView${horizontal}>`);
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
- // 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} }}>`);
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 radius = node.cornerRadius ?? 8;
319
- this.line(`<View style={[styles.card, { borderRadius: ${radius} }]}>`);
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.style, node.color);
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
- this.line(`<TouchableOpacity style={styles.button}${onPress}>`);
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={styles.buttonText}>${node.label}</Text>`);
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 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} }} />`);
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
- this.line(`{/* Icon: ${node.name} */}`);
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
- this.line(`<ActivityIndicator${centered} />`);
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(style, color) {
624
- const styleStr = `styles.${style}`;
625
- if (!color)
626
- return ` style={${styleStr}}`;
627
- return ` style={[${styleStr}, { color: '${color}' }]}`;
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
- stackStyleAttr(direction, spacing, padding) {
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
- if (this.peekWord() === 'spacing') {
707
- this.consumeWord();
708
- spacing = parseFloat(this.expect('NUMBER').value);
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
- if (this.peekWord() === 'corner') {
755
- this.consumeWord();
756
- this.expectWord('radius');
757
- cornerRadius = parseFloat(this.expect('NUMBER').value);
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
- if (this.peekWord() === 'color') {
791
- this.consumeWord();
792
- color = this.check('STRING') ? this.advance().value : this.expectWord();
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 StyleName]
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
- if (this.peekWord() === 'style') {
803
- this.consumeWord();
804
- style = this.expectWord();
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
- if (this.peekWord() === 'placeholder') {
818
- this.consumeWord();
819
- placeholder = this.expect('STRING').value;
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
- if (this.peekWord() === 'width') {
832
- this.consumeWord();
833
- width = parseFloat(this.expect('NUMBER').value);
834
- }
835
- if (this.peekWord() === 'height') {
836
- this.consumeWord();
837
- height = parseFloat(this.expect('NUMBER').value);
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
- // Everything else is layout
1350
+ // Layout nodes written directly (without layout: header)
1070
1351
  layout = this.parseLayoutNodes();
1071
1352
  }
1072
1353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "english-lang",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "The English (.eng) programming language compiler",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {