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 +34 -1
- package/dist/src/codegen/ReactNativeBackend.js +145 -28
- package/dist/src/parser/Parser.js +304 -48
- package/package.json +1 -1
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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: '
|
|
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() {
|