english-lang 0.2.2 → 0.2.4

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/cli/engc.js CHANGED
@@ -50,6 +50,32 @@ const fs = __importStar(require("fs"));
50
50
  const path = __importStar(require("path"));
51
51
  const childProcess = __importStar(require("child_process"));
52
52
  const index_1 = require("../index");
53
+ // ── Starter App.tsx ───────────────────────────────────────────
54
+ const STARTER_APP_TSX = `\
55
+ // App.tsx — wired up by engc init
56
+ // All screens are generated from .eng source files in screens/
57
+
58
+ import React from 'react';
59
+ import { NavigationContainer } from '@react-navigation/native';
60
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
61
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
62
+
63
+ import { HomeScreen } from './src/HomeScreen';
64
+
65
+ const Stack = createNativeStackNavigator();
66
+
67
+ export default function App() {
68
+ return (
69
+ <SafeAreaProvider>
70
+ <NavigationContainer>
71
+ <Stack.Navigator initialRouteName="Home">
72
+ <Stack.Screen name="Home" component={HomeScreen} />
73
+ </Stack.Navigator>
74
+ </NavigationContainer>
75
+ </SafeAreaProvider>
76
+ );
77
+ }
78
+ `;
53
79
  // ── Starter .eng file ─────────────────────────────────────────
54
80
  const STARTER_ENG = `\
55
81
  screen HomeScreen:
@@ -153,8 +179,15 @@ function initProject(name) {
153
179
  console.error('[engc] Failed to scaffold React Native app.');
154
180
  process.exit(1);
155
181
  }
182
+ // Install React Navigation
183
+ const rnDir = path.join(projectDir, 'rn');
184
+ console.log('\n[engc] Installing React Navigation ...');
185
+ childProcess.execSync('npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context', { cwd: rnDir, stdio: 'inherit' });
186
+ // Patch App.tsx to import the compiled HomeScreen
187
+ fs.writeFileSync(path.join(rnDir, 'App.tsx'), STARTER_APP_TSX, 'utf8');
188
+ console.log('[engc] Patched rn/App.tsx');
156
189
  // Pod install for iOS
157
- const iosDir = path.join(projectDir, 'rn', 'ios');
190
+ const iosDir = path.join(rnDir, 'ios');
158
191
  if (fs.existsSync(iosDir)) {
159
192
  console.log('\n[engc] Installing iOS CocoaPods ...');
160
193
  try {
@@ -60,6 +60,9 @@ 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) {
@@ -274,7 +277,7 @@ class ReactNativeBackend {
274
277
  emitLayoutNode(node, handlers, stateVars) {
275
278
  switch (node.kind) {
276
279
  case 'VerticalStack': {
277
- const styleAttr = this.stackStyleAttr('column', node.spacing, node.padding);
280
+ const styleAttr = this.buildStackStyle('column', node);
278
281
  this.line(`<View${styleAttr}>`);
279
282
  this.depth++;
280
283
  for (const child of node.children)
@@ -284,7 +287,7 @@ class ReactNativeBackend {
284
287
  break;
285
288
  }
286
289
  case 'HorizontalStack': {
287
- const styleAttr = this.stackStyleAttr('row', node.spacing, null);
290
+ const styleAttr = this.buildStackStyle('row', node);
288
291
  this.line(`<View${styleAttr}>`);
289
292
  this.depth++;
290
293
  for (const child of node.children)
@@ -295,7 +298,8 @@ class ReactNativeBackend {
295
298
  }
296
299
  case 'ScrollView': {
297
300
  const horizontal = node.direction === 'horizontal' ? ' horizontal' : '';
298
- this.line(`<ScrollView${horizontal}>`);
301
+ const svColor = node.color ? ` style={{ backgroundColor: '${node.color}' }}` : '';
302
+ this.line(`<ScrollView${horizontal}${svColor}>`);
299
303
  this.depth++;
300
304
  for (const child of node.children)
301
305
  this.emitLayoutNode(child, handlers, stateVars);
@@ -304,9 +308,12 @@ class ReactNativeBackend {
304
308
  break;
305
309
  }
306
310
  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} }}>`);
311
+ const gridParts = [`flexDirection: 'row'`, 'flexWrap: \'wrap\''];
312
+ if (node.spacing)
313
+ gridParts.push(`gap: ${node.spacing}`);
314
+ if (node.color)
315
+ gridParts.push(`backgroundColor: '${node.color}'`);
316
+ this.line(`<View style={{ ${gridParts.join(', ')} }}>`);
310
317
  this.depth++;
311
318
  for (const child of node.children)
312
319
  this.emitLayoutNode(child, handlers, stateVars);
@@ -315,8 +322,26 @@ class ReactNativeBackend {
315
322
  break;
316
323
  }
317
324
  case 'Card': {
318
- const radius = node.cornerRadius ?? 8;
319
- this.line(`<View style={[styles.card, { borderRadius: ${radius} }]}>`);
325
+ const cardParts = [];
326
+ if (node.cornerRadius != null)
327
+ cardParts.push(`borderRadius: ${node.cornerRadius}`);
328
+ if (node.color)
329
+ cardParts.push(`backgroundColor: '${node.color}'`);
330
+ if (node.borderColor)
331
+ cardParts.push(`borderColor: '${node.borderColor}', borderWidth: 1`);
332
+ const cardStyle = cardParts.length
333
+ ? `[styles.card, { ${cardParts.join(', ')} }]`
334
+ : 'styles.card';
335
+ this.line(`<View style={${cardStyle}}>`);
336
+ this.depth++;
337
+ for (const child of node.children)
338
+ this.emitLayoutNode(child, handlers, stateVars);
339
+ this.depth--;
340
+ this.line(`</View>`);
341
+ break;
342
+ }
343
+ case 'Overlay': {
344
+ this.line(`<View style={{ position: 'relative' }}>`);
320
345
  this.depth++;
321
346
  for (const child of node.children)
322
347
  this.emitLayoutNode(child, handlers, stateVars);
@@ -325,7 +350,7 @@ class ReactNativeBackend {
325
350
  break;
326
351
  }
327
352
  case 'Text': {
328
- const styleRef = this.textStyleRef(node.style, node.color);
353
+ const styleRef = this.textStyleRef(node);
329
354
  const content = this.exprToJSX(node.expr, stateVars);
330
355
  this.line(`<Text${styleRef}>${content}</Text>`);
331
356
  break;
@@ -333,16 +358,25 @@ class ReactNativeBackend {
333
358
  case 'Button': {
334
359
  const handler = handlers.get(node.label);
335
360
  const onPress = handler ? ` onPress={${handler}}` : ' onPress={() => {}}';
336
- this.line(`<TouchableOpacity style={styles.button}${onPress}>`);
361
+ const disabled = node.disabled ? ' disabled' : '';
362
+ const btnParts = [];
363
+ if (node.outline) {
364
+ btnParts.push(`borderWidth: 1`, `borderColor: '${node.color ?? '#007AFF'}'`, `backgroundColor: 'transparent'`);
365
+ }
366
+ else if (node.color) {
367
+ btnParts.push(`backgroundColor: '${node.color}'`);
368
+ }
369
+ const btnStyle = btnParts.length ? `[styles.button, { ${btnParts.join(', ')} }]` : 'styles.button';
370
+ const txtStyle = node.textColor ? `[styles.buttonText, { color: '${node.textColor}' }]` : 'styles.buttonText';
371
+ this.line(`<TouchableOpacity style={${btnStyle}}${onPress}${disabled}>`);
337
372
  this.depth++;
338
- this.line(`<Text style={styles.buttonText}>${node.label}</Text>`);
373
+ this.line(`<Text style={${txtStyle}}>${node.label}</Text>`);
339
374
  this.depth--;
340
375
  this.line(`</TouchableOpacity>`);
341
376
  break;
342
377
  }
343
378
  case 'TextField': {
344
379
  const setter = setterName(node.boundTo);
345
- const placeholder = node.placeholder ? ` placeholder="${node.placeholder}"` : '';
346
380
  this.line(`<TextInput`);
347
381
  this.depth++;
348
382
  this.line(`style={styles.input}`);
@@ -350,20 +384,38 @@ class ReactNativeBackend {
350
384
  this.line(`onChangeText={${setter}}`);
351
385
  if (node.placeholder)
352
386
  this.line(`placeholder="${node.placeholder}"`);
387
+ if (node.multiline)
388
+ this.line(`multiline`);
389
+ if (node.secure)
390
+ this.line(`secureTextEntry`);
391
+ if (node.keyboard)
392
+ this.line(`keyboardType="${node.keyboard}"`);
353
393
  this.depth--;
354
394
  this.line(`/>`);
355
395
  break;
356
396
  }
357
397
  case 'Image': {
358
398
  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} }} />`);
399
+ const imgParts = [];
400
+ if (node.width)
401
+ imgParts.push(`width: ${node.width}`);
402
+ if (node.height)
403
+ imgParts.push(`height: ${node.height}`);
404
+ if (node.rounded) {
405
+ const r = Math.min(node.width ?? 50, node.height ?? 50) / 2;
406
+ imgParts.push(`borderRadius: ${r}`);
407
+ }
408
+ const imgStyle = imgParts.length ? `{ ${imgParts.join(', ')} }` : '{}';
409
+ this.line(`<Image source={{ uri: ${src} }} style={${imgStyle}} />`);
362
410
  break;
363
411
  }
364
- case 'Icon':
365
- this.line(`{/* Icon: ${node.name} */}`);
412
+ case 'Icon': {
413
+ const size = node.size ?? 24;
414
+ const color = node.color ? ` color="${node.color}"` : '';
415
+ this.line(`{/* Icon: ${node.name} — install react-native-vector-icons */}`);
416
+ this.line(`{/* <Icon name="${node.name}" size={${size}}${color} /> */}`);
366
417
  break;
418
+ }
367
419
  case 'Spacer':
368
420
  this.line(`<View style={{ flex: 1 }} />`);
369
421
  break;
@@ -372,7 +424,30 @@ class ReactNativeBackend {
372
424
  break;
373
425
  case 'LoadingSpinner': {
374
426
  const centered = node.centered ? ` style={{ alignSelf: 'center' }}` : '';
375
- this.line(`<ActivityIndicator${centered} />`);
427
+ const color = node.color ? ` color="${node.color}"` : '';
428
+ this.line(`<ActivityIndicator${centered}${color} />`);
429
+ break;
430
+ }
431
+ case 'Switch': {
432
+ const setter = setterName(node.boundTo);
433
+ const trackColor = node.color
434
+ ? ` trackColor={{ false: '#767577', true: '${node.color}' }}`
435
+ : '';
436
+ this.line(`<Switch value={${node.boundTo}} onValueChange={${setter}}${trackColor} />`);
437
+ break;
438
+ }
439
+ case 'Slider': {
440
+ const setter = setterName(node.boundTo);
441
+ this.line(`<Slider`);
442
+ this.depth++;
443
+ this.line(`minimumValue={${node.min}}`);
444
+ this.line(`maximumValue={${node.max}}`);
445
+ if (node.step != null)
446
+ this.line(`step={${node.step}}`);
447
+ this.line(`value={${node.boundTo}}`);
448
+ this.line(`onValueChange={${setter}}`);
449
+ this.depth--;
450
+ this.line(`/>`);
376
451
  break;
377
452
  }
378
453
  case 'ForEach': {
@@ -572,6 +647,24 @@ class ReactNativeBackend {
572
647
  };
573
648
  return map[name] ?? name;
574
649
  }
650
+ hasSlider(layout) {
651
+ const scan = (nodes) => {
652
+ for (const node of nodes) {
653
+ if (node.kind === 'Slider')
654
+ return true;
655
+ if ('children' in node && scan(node.children))
656
+ return true;
657
+ if ('thenBranch' in node) {
658
+ if (scan(node.thenBranch))
659
+ return true;
660
+ if (node.elseBranch && scan(node.elseBranch))
661
+ return true;
662
+ }
663
+ }
664
+ return false;
665
+ };
666
+ return scan(layout);
667
+ }
575
668
  buildButtonHandlerMap(events) {
576
669
  const map = new Map();
577
670
  for (const e of events) {
@@ -605,6 +698,10 @@ class ReactNativeBackend {
605
698
  case 'ScrollView':
606
699
  needed.add('ScrollView');
607
700
  break;
701
+ case 'Switch':
702
+ needed.add('Switch');
703
+ break;
704
+ // Slider comes from @react-native-community/slider, imported separately
608
705
  }
609
706
  if ('children' in node)
610
707
  scan(node.children);
@@ -620,18 +717,38 @@ class ReactNativeBackend {
620
717
  scan(layout);
621
718
  return Array.from(needed).sort();
622
719
  }
623
- textStyleRef(style, color) {
624
- const styleStr = `styles.${style}`;
625
- if (!color)
626
- return ` style={${styleStr}}`;
627
- return ` style={[${styleStr}, { color: '${color}' }]}`;
720
+ textStyleRef(node) {
721
+ const inline = [];
722
+ if (node.color)
723
+ inline.push(`color: '${node.color}'`);
724
+ if (node.size)
725
+ inline.push(`fontSize: ${node.size}`);
726
+ if (node.bold)
727
+ inline.push(`fontWeight: 'bold' as const`);
728
+ if (node.italic)
729
+ inline.push(`fontStyle: 'italic' as const`);
730
+ if (inline.length === 0)
731
+ return ` style={styles.${node.style}}`;
732
+ return ` style={[styles.${node.style}, { ${inline.join(', ')} }]}`;
628
733
  }
629
- stackStyleAttr(direction, spacing, padding) {
734
+ buildStackStyle(direction, node) {
630
735
  const parts = [`flexDirection: '${direction}'`];
631
- if (spacing)
632
- parts.push(`gap: ${spacing}`);
633
- if (padding)
634
- parts.push(`padding: ${padding}`);
736
+ if (node.spacing)
737
+ parts.push(`gap: ${node.spacing}`);
738
+ if (node.padding)
739
+ parts.push(`padding: ${node.padding}`);
740
+ if (node.color)
741
+ parts.push(`backgroundColor: '${node.color}'`);
742
+ if (node.borderColor)
743
+ parts.push(`borderColor: '${node.borderColor}', borderWidth: 1`);
744
+ if (node.borderRadius)
745
+ parts.push(`borderRadius: ${node.borderRadius}`);
746
+ if (node.opacity)
747
+ parts.push(`opacity: ${node.opacity}`);
748
+ if (node.align)
749
+ parts.push(`alignItems: '${node.align}'`);
750
+ if (node.justify)
751
+ parts.push(`justifyContent: '${node.justify}'`);
635
752
  return ` style={{ ${parts.join(', ')} }}`;
636
753
  }
637
754
  // ── Output helpers ────────────────────────────────────────
@@ -663,6 +663,12 @@ class Parser {
663
663
  }
664
664
  if (word === 'loading')
665
665
  return this.parseLoadingSpinner();
666
+ if (word === 'switch')
667
+ return this.parseSwitch();
668
+ if (word === 'slider')
669
+ return this.parseSlider();
670
+ if (word === 'overlay')
671
+ return this.parseOverlay();
666
672
  if (word === 'if')
667
673
  return this.parseLayoutIf();
668
674
  if (word === 'for')
@@ -672,12 +678,18 @@ class Parser {
672
678
  return this.parseComponentCall();
673
679
  throw this.error(`Unknown layout element "${word}"`);
674
680
  }
675
- // vertical stack [spacing N] [padding N]:
681
+ // vertical stack [spacing N] [padding N] [color X] [border color X] [rounded N] [opacity N] [align X] [justify X]:
676
682
  parseVerticalStack() {
677
683
  this.expectWord('vertical');
678
684
  this.expectWord('stack');
679
685
  let spacing = null;
680
686
  let padding = null;
687
+ let color = null;
688
+ let borderColor = null;
689
+ let borderRadius = null;
690
+ let opacity = null;
691
+ let align = null;
692
+ let justify = null;
681
693
  while (this.check('WORD')) {
682
694
  const kw = this.peekWord();
683
695
  if (kw === 'spacing') {
@@ -688,6 +700,36 @@ class Parser {
688
700
  this.consumeWord();
689
701
  padding = parseFloat(this.expect('NUMBER').value);
690
702
  }
703
+ else if (kw === 'color') {
704
+ this.consumeWord();
705
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
706
+ }
707
+ else if (kw === 'border') {
708
+ this.consumeWord();
709
+ if (this.peekWord() === 'color') {
710
+ this.consumeWord();
711
+ borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
712
+ }
713
+ else {
714
+ borderRadius = parseFloat(this.expect('NUMBER').value);
715
+ }
716
+ }
717
+ else if (kw === 'rounded') {
718
+ this.consumeWord();
719
+ borderRadius = parseFloat(this.expect('NUMBER').value);
720
+ }
721
+ else if (kw === 'opacity') {
722
+ this.consumeWord();
723
+ opacity = parseFloat(this.expect('NUMBER').value);
724
+ }
725
+ else if (kw === 'align') {
726
+ this.consumeWord();
727
+ align = this.expectWord();
728
+ }
729
+ else if (kw === 'justify') {
730
+ this.consumeWord();
731
+ justify = this.expectWord();
732
+ }
691
733
  else
692
734
  break;
693
735
  }
@@ -696,77 +738,150 @@ class Parser {
696
738
  this.expect('INDENT');
697
739
  const children = this.parseLayoutNodes();
698
740
  this.expect('DEDENT');
699
- return { kind: 'VerticalStack', spacing, padding, children };
741
+ return { kind: 'VerticalStack', spacing, padding, color, borderColor, borderRadius, opacity, align, justify, children };
700
742
  }
701
- // horizontal stack [spacing N]:
743
+ // horizontal stack [spacing N] [padding N] [color X] [border color X] [rounded N] [opacity N] [align X] [justify X]:
702
744
  parseHorizontalStack() {
703
745
  this.expectWord('horizontal');
704
746
  this.expectWord('stack');
705
747
  let spacing = null;
706
- if (this.peekWord() === 'spacing') {
707
- this.consumeWord();
708
- spacing = parseFloat(this.expect('NUMBER').value);
748
+ let padding = null;
749
+ let color = null;
750
+ let borderColor = null;
751
+ let borderRadius = null;
752
+ let opacity = null;
753
+ let align = null;
754
+ let justify = null;
755
+ while (this.check('WORD')) {
756
+ const kw = this.peekWord();
757
+ if (kw === 'spacing') {
758
+ this.consumeWord();
759
+ spacing = parseFloat(this.expect('NUMBER').value);
760
+ }
761
+ else if (kw === 'padding') {
762
+ this.consumeWord();
763
+ padding = parseFloat(this.expect('NUMBER').value);
764
+ }
765
+ else if (kw === 'color') {
766
+ this.consumeWord();
767
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
768
+ }
769
+ else if (kw === 'border') {
770
+ this.consumeWord();
771
+ if (this.peekWord() === 'color') {
772
+ this.consumeWord();
773
+ borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
774
+ }
775
+ else {
776
+ borderRadius = parseFloat(this.expect('NUMBER').value);
777
+ }
778
+ }
779
+ else if (kw === 'rounded') {
780
+ this.consumeWord();
781
+ borderRadius = parseFloat(this.expect('NUMBER').value);
782
+ }
783
+ else if (kw === 'opacity') {
784
+ this.consumeWord();
785
+ opacity = parseFloat(this.expect('NUMBER').value);
786
+ }
787
+ else if (kw === 'align') {
788
+ this.consumeWord();
789
+ align = this.expectWord();
790
+ }
791
+ else if (kw === 'justify') {
792
+ this.consumeWord();
793
+ justify = this.expectWord();
794
+ }
795
+ else
796
+ break;
709
797
  }
710
798
  this.expect('COLON');
711
799
  this.expect('NEWLINE');
712
800
  this.expect('INDENT');
713
801
  const children = this.parseLayoutNodes();
714
802
  this.expect('DEDENT');
715
- return { kind: 'HorizontalStack', spacing, children };
803
+ return { kind: 'HorizontalStack', spacing, padding, color, borderColor, borderRadius, opacity, align, justify, children };
716
804
  }
717
- // scroll view [vertical|horizontal]:
805
+ // scroll view [vertical|horizontal] [color X]:
718
806
  parseScrollView() {
719
807
  this.expectWord('scroll');
720
808
  this.expectWord('view');
721
809
  let direction = 'vertical';
810
+ let color = null;
722
811
  if (this.peekWord() === 'vertical' || this.peekWord() === 'horizontal') {
723
812
  direction = this.consumeWord();
724
813
  }
814
+ if (this.peekWord() === 'color') {
815
+ this.consumeWord();
816
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
817
+ }
725
818
  this.expect('COLON');
726
819
  this.expect('NEWLINE');
727
820
  this.expect('INDENT');
728
821
  const children = this.parseLayoutNodes();
729
822
  this.expect('DEDENT');
730
- return { kind: 'ScrollView', direction, children };
823
+ return { kind: 'ScrollView', direction, color, children };
731
824
  }
732
- // grid with N columns [spacing N]:
825
+ // grid with N columns [spacing N] [color X]:
733
826
  parseGrid() {
734
827
  this.expectWord('grid');
735
828
  this.expectWord('with');
736
829
  const columns = parseFloat(this.expect('NUMBER').value);
737
830
  this.expectWord('columns');
738
831
  let spacing = null;
832
+ let color = null;
739
833
  if (this.peekWord() === 'spacing') {
740
834
  this.consumeWord();
741
835
  spacing = parseFloat(this.expect('NUMBER').value);
742
836
  }
837
+ if (this.peekWord() === 'color') {
838
+ this.consumeWord();
839
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
840
+ }
743
841
  this.expect('COLON');
744
842
  this.expect('NEWLINE');
745
843
  this.expect('INDENT');
746
844
  const children = this.parseLayoutNodes();
747
845
  this.expect('DEDENT');
748
- return { kind: 'Grid', columns, spacing, children };
846
+ return { kind: 'Grid', columns, spacing, color, children };
749
847
  }
750
- // card [corner radius N]:
848
+ // card [corner radius N] [color X] [border color X]:
751
849
  parseCard() {
752
850
  this.expectWord('card');
753
851
  let cornerRadius = null;
754
- if (this.peekWord() === 'corner') {
755
- this.consumeWord();
756
- this.expectWord('radius');
757
- cornerRadius = parseFloat(this.expect('NUMBER').value);
852
+ let color = null;
853
+ let borderColor = null;
854
+ while (this.check('WORD')) {
855
+ const kw = this.peekWord();
856
+ if (kw === 'corner') {
857
+ this.consumeWord();
858
+ this.expectWord('radius');
859
+ cornerRadius = parseFloat(this.expect('NUMBER').value);
860
+ }
861
+ else if (kw === 'color') {
862
+ this.consumeWord();
863
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
864
+ }
865
+ else if (kw === 'border') {
866
+ this.consumeWord();
867
+ if (this.peekWord() === 'color') {
868
+ this.consumeWord();
869
+ borderColor = this.check('STRING') ? this.advance().value : this.expectWord();
870
+ }
871
+ }
872
+ else {
873
+ break;
874
+ }
758
875
  }
759
876
  this.expect('COLON');
760
877
  this.expect('NEWLINE');
761
878
  this.expect('INDENT');
762
879
  const children = this.parseLayoutNodes();
763
880
  this.expect('DEDENT');
764
- return { kind: 'Card', cornerRadius, children };
881
+ return { kind: 'Card', cornerRadius, color, borderColor, children };
765
882
  }
766
- // show "text" [as style] [color X]
767
- // show variable [as style] [color X]
768
- // text "text" as style
769
- // text variable as style
883
+ // show "text" [as style] [color X] [size N] [bold] [italic]
884
+ // show variable [as style] [color X] [size N] [bold] [italic]
770
885
  parseTextNode() {
771
886
  const kw = this.consumeWord(); // 'show' or 'text'
772
887
  if (kw !== 'show' && kw !== 'text')
@@ -787,26 +902,74 @@ class Parser {
787
902
  style = this.expectWord();
788
903
  }
789
904
  let color = null;
790
- if (this.peekWord() === 'color') {
791
- this.consumeWord();
792
- color = this.check('STRING') ? this.advance().value : this.expectWord();
905
+ let size = null;
906
+ let bold = false;
907
+ let italic = false;
908
+ while (this.check('WORD')) {
909
+ const mod = this.peekWord();
910
+ if (mod === 'color') {
911
+ this.consumeWord();
912
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
913
+ }
914
+ else if (mod === 'size') {
915
+ this.consumeWord();
916
+ size = parseFloat(this.expect('NUMBER').value);
917
+ }
918
+ else if (mod === 'bold') {
919
+ this.consumeWord();
920
+ bold = true;
921
+ }
922
+ else if (mod === 'italic') {
923
+ this.consumeWord();
924
+ italic = true;
925
+ }
926
+ else
927
+ break;
793
928
  }
794
929
  this.expectNewline();
795
- return { kind: 'Text', expr, style, color };
930
+ return { kind: 'Text', expr, style, color, size, bold, italic };
796
931
  }
797
- // button "Label" [style StyleName]
932
+ // button "Label" [style X] [color X] [text color X] [disabled] [outline]
798
933
  parseButton() {
799
934
  this.expectWord('button');
800
935
  const label = this.expect('STRING').value;
801
936
  let style = null;
802
- if (this.peekWord() === 'style') {
803
- this.consumeWord();
804
- style = this.expectWord();
937
+ let color = null;
938
+ let textColor = null;
939
+ let disabled = false;
940
+ let outline = false;
941
+ while (this.check('WORD')) {
942
+ const kw = this.peekWord();
943
+ if (kw === 'style') {
944
+ this.consumeWord();
945
+ style = this.expectWord();
946
+ }
947
+ else if (kw === 'color') {
948
+ this.consumeWord();
949
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
950
+ }
951
+ else if (kw === 'text') {
952
+ this.consumeWord();
953
+ if (this.peekWord() === 'color') {
954
+ this.consumeWord();
955
+ textColor = this.check('STRING') ? this.advance().value : this.expectWord();
956
+ }
957
+ }
958
+ else if (kw === 'disabled') {
959
+ this.consumeWord();
960
+ disabled = true;
961
+ }
962
+ else if (kw === 'outline') {
963
+ this.consumeWord();
964
+ outline = true;
965
+ }
966
+ else
967
+ break;
805
968
  }
806
969
  this.expectNewline();
807
- return { kind: 'Button', label, style };
970
+ return { kind: 'Button', label, style, color, textColor, disabled, outline };
808
971
  }
809
- // text field bound to varName [placeholder "..."]
972
+ // text field bound to varName [placeholder "..."] [multiline] [secure] [keyboard X]
810
973
  parseTextField() {
811
974
  this.expectWord('text');
812
975
  this.expectWord('field');
@@ -814,55 +977,148 @@ class Parser {
814
977
  this.expectWord('to');
815
978
  const boundTo = this.expectWord();
816
979
  let placeholder = null;
817
- if (this.peekWord() === 'placeholder') {
818
- this.consumeWord();
819
- placeholder = this.expect('STRING').value;
980
+ let multiline = false;
981
+ let secure = false;
982
+ let keyboard = null;
983
+ while (this.check('WORD')) {
984
+ const kw = this.peekWord();
985
+ if (kw === 'placeholder') {
986
+ this.consumeWord();
987
+ placeholder = this.expect('STRING').value;
988
+ }
989
+ else if (kw === 'multiline') {
990
+ this.consumeWord();
991
+ multiline = true;
992
+ }
993
+ else if (kw === 'secure') {
994
+ this.consumeWord();
995
+ secure = true;
996
+ }
997
+ else if (kw === 'keyboard') {
998
+ this.consumeWord();
999
+ keyboard = this.expectWord();
1000
+ }
1001
+ else
1002
+ break;
820
1003
  }
821
1004
  this.expectNewline();
822
- return { kind: 'TextField', placeholder, boundTo };
1005
+ return { kind: 'TextField', placeholder, boundTo, multiline, secure, keyboard };
823
1006
  }
824
- // image from expr [width N] [height N]
1007
+ // image from expr [width N] [height N] [rounded]
825
1008
  parseImage() {
826
1009
  this.expectWord('image');
827
1010
  this.expectWord('from');
828
1011
  const source = this.parseExpr();
829
1012
  let width = null;
830
1013
  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);
1014
+ let rounded = false;
1015
+ while (this.check('WORD')) {
1016
+ const kw = this.peekWord();
1017
+ if (kw === 'width') {
1018
+ this.consumeWord();
1019
+ width = parseFloat(this.expect('NUMBER').value);
1020
+ }
1021
+ else if (kw === 'height') {
1022
+ this.consumeWord();
1023
+ height = parseFloat(this.expect('NUMBER').value);
1024
+ }
1025
+ else if (kw === 'rounded') {
1026
+ this.consumeWord();
1027
+ rounded = true;
1028
+ }
1029
+ else
1030
+ break;
838
1031
  }
839
1032
  this.expectNewline();
840
- return { kind: 'Image', source, width, height };
1033
+ return { kind: 'Image', source, width, height, rounded };
841
1034
  }
842
- // icon named "name" [size N]
1035
+ // icon named "name" [size N] [color X]
843
1036
  parseIcon() {
844
1037
  this.expectWord('icon');
845
1038
  this.expectWord('named');
846
1039
  const name = this.expect('STRING').value;
847
1040
  let size = null;
1041
+ let color = null;
848
1042
  if (this.peekWord() === 'size') {
849
1043
  this.consumeWord();
850
1044
  size = parseFloat(this.expect('NUMBER').value);
851
1045
  }
1046
+ if (this.peekWord() === 'color') {
1047
+ this.consumeWord();
1048
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
1049
+ }
852
1050
  this.expectNewline();
853
- return { kind: 'Icon', name, size };
1051
+ return { kind: 'Icon', name, size, color };
854
1052
  }
855
- // loading spinner [centered]
1053
+ // loading spinner [centered] [color X]
856
1054
  parseLoadingSpinner() {
857
1055
  this.expectWord('loading');
858
1056
  this.expectWord('spinner');
859
1057
  let centered = false;
1058
+ let color = null;
860
1059
  if (this.peekWord() === 'centered') {
861
1060
  this.consumeWord();
862
1061
  centered = true;
863
1062
  }
1063
+ if (this.peekWord() === 'color') {
1064
+ this.consumeWord();
1065
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
1066
+ }
1067
+ this.expectNewline();
1068
+ return { kind: 'LoadingSpinner', centered, color };
1069
+ }
1070
+ // switch bound to varName [color X]
1071
+ parseSwitch() {
1072
+ this.expectWord('switch');
1073
+ this.expectWord('bound');
1074
+ this.expectWord('to');
1075
+ const boundTo = this.expectWord();
1076
+ let color = null;
1077
+ if (this.peekWord() === 'color') {
1078
+ this.consumeWord();
1079
+ color = this.check('STRING') ? this.advance().value : this.expectWord();
1080
+ }
864
1081
  this.expectNewline();
865
- return { kind: 'LoadingSpinner', centered };
1082
+ return { kind: 'Switch', boundTo, color };
1083
+ }
1084
+ // slider bound to varName [min N] [max N] [step N]
1085
+ parseSlider() {
1086
+ this.expectWord('slider');
1087
+ this.expectWord('bound');
1088
+ this.expectWord('to');
1089
+ const boundTo = this.expectWord();
1090
+ let min = 0;
1091
+ let max = 100;
1092
+ let step = null;
1093
+ while (this.check('WORD')) {
1094
+ const kw = this.peekWord();
1095
+ if (kw === 'min') {
1096
+ this.consumeWord();
1097
+ min = parseFloat(this.expect('NUMBER').value);
1098
+ }
1099
+ else if (kw === 'max') {
1100
+ this.consumeWord();
1101
+ max = parseFloat(this.expect('NUMBER').value);
1102
+ }
1103
+ else if (kw === 'step') {
1104
+ this.consumeWord();
1105
+ step = parseFloat(this.expect('NUMBER').value);
1106
+ }
1107
+ else
1108
+ break;
1109
+ }
1110
+ this.expectNewline();
1111
+ return { kind: 'Slider', boundTo, min, max, step };
1112
+ }
1113
+ // overlay:
1114
+ parseOverlay() {
1115
+ this.expectWord('overlay');
1116
+ this.expect('COLON');
1117
+ this.expect('NEWLINE');
1118
+ this.expect('INDENT');
1119
+ const children = this.parseLayoutNodes();
1120
+ this.expect('DEDENT');
1121
+ return { kind: 'Overlay', children };
866
1122
  }
867
1123
  // if condition: ... [otherwise if condition: ...] [otherwise: ...]
868
1124
  parseLayoutIf() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "english-lang",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "The English (.eng) programming language compiler",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {