gelang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- package/scripts/check-toolchains.py +85 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""GE UI DSL — a declarative UI designer language.
|
|
2
|
+
|
|
3
|
+
This is a separate language from the major backends (C++, Rust, Dart, C#,
|
|
4
|
+
Zig, Go). It uses a simple, readable syntax to describe UI screens that
|
|
5
|
+
compile to Flutter widgets. It can be combined with any backend when
|
|
6
|
+
building a complete application.
|
|
7
|
+
|
|
8
|
+
Syntax example (.ge.ui file):
|
|
9
|
+
|
|
10
|
+
Screen "MyRent Dashboard" {
|
|
11
|
+
state: total_rent: int = 0
|
|
12
|
+
state: total_paid: int = 0
|
|
13
|
+
state: outstanding: int = 0
|
|
14
|
+
|
|
15
|
+
Column {
|
|
16
|
+
padding: 16
|
|
17
|
+
children:
|
|
18
|
+
Text "Total Rent" style="headline"
|
|
19
|
+
Text state="total_rent" style="value"
|
|
20
|
+
Divider
|
|
21
|
+
Text "Total Paid" style="headline"
|
|
22
|
+
Text state="total_paid" style="value"
|
|
23
|
+
SizedBox height=20
|
|
24
|
+
ElevatedButton "Refresh" on_click=Action(call="compute_total_rent")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
The DSL supports:
|
|
29
|
+
- Screen definitions with title and state declarations
|
|
30
|
+
- Container widgets: Column, Row, Container, Expanded, SizedBox
|
|
31
|
+
- Display widgets: Text, Divider, Icon
|
|
32
|
+
- Interactive widgets: ElevatedButton, TextField
|
|
33
|
+
- Style strings: "fontSize:20; bold:true; color:#1565C0"
|
|
34
|
+
- State bindings: state="variable_name"
|
|
35
|
+
- Actions: on_click=Action(call="function_name", args=[...], update="state_var")
|
|
36
|
+
- Named styles: headline, value, title, subtitle, error
|
|
37
|
+
|
|
38
|
+
The parser produces a tree of WidgetNode dicts that can be converted to
|
|
39
|
+
Flutter Dart code by the dartgen module.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WidgetNode:
|
|
50
|
+
"""A node in the UI widget tree."""
|
|
51
|
+
kind: str
|
|
52
|
+
children: list["WidgetNode"] = field(default_factory=list)
|
|
53
|
+
props: dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
style_dict: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
state: str | None = None
|
|
56
|
+
text: str | None = None
|
|
57
|
+
label: str | None = None
|
|
58
|
+
action: dict[str, Any] | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class StateVar:
|
|
63
|
+
"""A state variable declaration."""
|
|
64
|
+
name: str
|
|
65
|
+
type: str
|
|
66
|
+
default: Any = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Screen:
|
|
71
|
+
"""A UI screen definition."""
|
|
72
|
+
title: str
|
|
73
|
+
state_vars: list[StateVar] = field(default_factory=list)
|
|
74
|
+
root: WidgetNode | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---- Tokenizer ----
|
|
78
|
+
|
|
79
|
+
class Token:
|
|
80
|
+
def __init__(self, kind: str, value: str, line: int):
|
|
81
|
+
self.kind = kind
|
|
82
|
+
self.value = value
|
|
83
|
+
self.line = line
|
|
84
|
+
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return f"Token({self.kind}, {self.value!r}, line={self.line})"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tokenize(source: str) -> list[Token]:
|
|
90
|
+
"""Tokenize the UI DSL source."""
|
|
91
|
+
tokens: list[Token] = []
|
|
92
|
+
line = 1
|
|
93
|
+
i = 0
|
|
94
|
+
while i < len(source):
|
|
95
|
+
c = source[i]
|
|
96
|
+
if c == "\n":
|
|
97
|
+
line += 1
|
|
98
|
+
i += 1
|
|
99
|
+
continue
|
|
100
|
+
if c in " \t\r":
|
|
101
|
+
i += 1
|
|
102
|
+
continue
|
|
103
|
+
if c == "#": # comment
|
|
104
|
+
while i < len(source) and source[i] != "\n":
|
|
105
|
+
i += 1
|
|
106
|
+
continue
|
|
107
|
+
if source.startswith('"""', i) or source.startswith("'''", i):
|
|
108
|
+
# module/block docstring — skip to the closing triple quote
|
|
109
|
+
quote = source[i:i + 3]
|
|
110
|
+
end = source.find(quote, i + 3)
|
|
111
|
+
if end == -1:
|
|
112
|
+
end = len(source)
|
|
113
|
+
line += source.count("\n", i, end)
|
|
114
|
+
i = end + 3
|
|
115
|
+
continue
|
|
116
|
+
if c == "{":
|
|
117
|
+
tokens.append(Token("LBRACE", "{", line))
|
|
118
|
+
i += 1
|
|
119
|
+
continue
|
|
120
|
+
if c == "}":
|
|
121
|
+
tokens.append(Token("RBRACE", "}", line))
|
|
122
|
+
i += 1
|
|
123
|
+
continue
|
|
124
|
+
if c == ":":
|
|
125
|
+
tokens.append(Token("COLON", ":", line))
|
|
126
|
+
i += 1
|
|
127
|
+
continue
|
|
128
|
+
if c == "=":
|
|
129
|
+
tokens.append(Token("EQUALS", "=", line))
|
|
130
|
+
i += 1
|
|
131
|
+
continue
|
|
132
|
+
if c == "[":
|
|
133
|
+
tokens.append(Token("LBRACKET", "[", line))
|
|
134
|
+
i += 1
|
|
135
|
+
continue
|
|
136
|
+
if c == "]":
|
|
137
|
+
tokens.append(Token("RBRACKET", "]", line))
|
|
138
|
+
i += 1
|
|
139
|
+
continue
|
|
140
|
+
if c == ",":
|
|
141
|
+
tokens.append(Token("COMMA", ",", line))
|
|
142
|
+
i += 1
|
|
143
|
+
continue
|
|
144
|
+
if c == "(":
|
|
145
|
+
tokens.append(Token("LPAREN", "(", line))
|
|
146
|
+
i += 1
|
|
147
|
+
continue
|
|
148
|
+
if c == ")":
|
|
149
|
+
tokens.append(Token("RPAREN", ")", line))
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
if c == '"':
|
|
153
|
+
# string literal
|
|
154
|
+
j = i + 1
|
|
155
|
+
while j < len(source) and source[j] != '"':
|
|
156
|
+
if source[j] == "\\":
|
|
157
|
+
j += 1
|
|
158
|
+
j += 1
|
|
159
|
+
tokens.append(Token("STRING", source[i + 1:j], line))
|
|
160
|
+
i = j + 1
|
|
161
|
+
continue
|
|
162
|
+
if c.isdigit() or (c == "-" and i + 1 < len(source) and source[i + 1].isdigit()):
|
|
163
|
+
# number literal
|
|
164
|
+
j = i + 1
|
|
165
|
+
while j < len(source) and (source[j].isdigit() or source[j] == "."):
|
|
166
|
+
j += 1
|
|
167
|
+
tokens.append(Token("NUMBER", source[i:j], line))
|
|
168
|
+
i = j
|
|
169
|
+
continue
|
|
170
|
+
if c.isalpha() or c == "_":
|
|
171
|
+
# identifier or keyword
|
|
172
|
+
j = i + 1
|
|
173
|
+
while j < len(source) and (source[j].isalnum() or source[j] in "_."):
|
|
174
|
+
j += 1
|
|
175
|
+
word = source[i:j]
|
|
176
|
+
tokens.append(Token("IDENT", word, line))
|
|
177
|
+
i = j
|
|
178
|
+
continue
|
|
179
|
+
raise SyntaxError(f"Unexpected character {c!r} at line {line}")
|
|
180
|
+
|
|
181
|
+
tokens.append(Token("EOF", "", line))
|
|
182
|
+
return tokens
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---- Parser ----
|
|
186
|
+
|
|
187
|
+
CONTAINER_WIDGETS = {"Column", "Row", "Container", "Expanded", "SizedBox",
|
|
188
|
+
"ListView", "Stack", "Padding", "Center", "Card",
|
|
189
|
+
"Scaffold", "AppBar"}
|
|
190
|
+
LEAF_WIDGETS = {"Text", "Divider", "Icon", "ElevatedButton", "TextButton",
|
|
191
|
+
"TextField", "Image", "CircularProgressIndicator",
|
|
192
|
+
"Switch", "Checkbox", "Slider"}
|
|
193
|
+
ALL_WIDGETS = CONTAINER_WIDGETS | LEAF_WIDGETS
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Parser:
|
|
197
|
+
def __init__(self, tokens: list[Token]):
|
|
198
|
+
self.tokens = tokens
|
|
199
|
+
self.pos = 0
|
|
200
|
+
|
|
201
|
+
def peek(self) -> Token:
|
|
202
|
+
return self.tokens[self.pos]
|
|
203
|
+
|
|
204
|
+
def next(self) -> Token:
|
|
205
|
+
t = self.tokens[self.pos]
|
|
206
|
+
self.pos += 1
|
|
207
|
+
return t
|
|
208
|
+
|
|
209
|
+
def expect(self, kind: str) -> Token:
|
|
210
|
+
t = self.next()
|
|
211
|
+
if t.kind != kind:
|
|
212
|
+
raise SyntaxError(f"Expected {kind} but got {t.kind} ({t.value!r}) at line {t.line}")
|
|
213
|
+
return t
|
|
214
|
+
|
|
215
|
+
def parse_screen(self) -> Screen:
|
|
216
|
+
"""Parse a screen definition.
|
|
217
|
+
|
|
218
|
+
Both forms are accepted:
|
|
219
|
+
Screen "My App" { ... }
|
|
220
|
+
Window { title: "My App" ... }
|
|
221
|
+
"""
|
|
222
|
+
self.expect("IDENT") # "Screen" / "Window"
|
|
223
|
+
title = ""
|
|
224
|
+
if self.peek().kind == "STRING":
|
|
225
|
+
title = self.next().value
|
|
226
|
+
screen = Screen(title=title)
|
|
227
|
+
self.expect("LBRACE")
|
|
228
|
+
|
|
229
|
+
while self.peek().kind != "RBRACE":
|
|
230
|
+
t = self.peek()
|
|
231
|
+
if t.kind == "IDENT" and t.value in ("title", "name"):
|
|
232
|
+
self.next() # consume "title" / "name"
|
|
233
|
+
self.expect("COLON")
|
|
234
|
+
screen.title = self.expect("STRING").value
|
|
235
|
+
elif t.kind == "IDENT" and t.value == "state":
|
|
236
|
+
self.next() # consume "state"
|
|
237
|
+
self.expect("COLON")
|
|
238
|
+
state_var = self.parse_state_decl()
|
|
239
|
+
screen.state_vars.append(state_var)
|
|
240
|
+
elif t.kind == "IDENT" and t.value in ALL_WIDGETS:
|
|
241
|
+
widget = self.parse_widget()
|
|
242
|
+
screen.root = widget
|
|
243
|
+
elif t.kind == "IDENT" and t.value == "children":
|
|
244
|
+
self.next() # consume "children"
|
|
245
|
+
self.expect("COLON")
|
|
246
|
+
# parse child widgets until we hit something that's not a widget
|
|
247
|
+
while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
|
|
248
|
+
child = self.parse_widget()
|
|
249
|
+
if screen.root is None:
|
|
250
|
+
screen.root = WidgetNode(kind="Column")
|
|
251
|
+
screen.root.children.append(child)
|
|
252
|
+
else:
|
|
253
|
+
raise SyntaxError(f"Unexpected token {t.value!r} at line {t.line}")
|
|
254
|
+
|
|
255
|
+
self.expect("RBRACE")
|
|
256
|
+
return screen
|
|
257
|
+
|
|
258
|
+
def parse_state_decl(self) -> StateVar:
|
|
259
|
+
"""Parse a state variable declaration: name: type = default"""
|
|
260
|
+
name_tok = self.expect("IDENT")
|
|
261
|
+
self.expect("COLON")
|
|
262
|
+
type_tok = self.expect("IDENT")
|
|
263
|
+
default = None
|
|
264
|
+
if self.peek().kind == "EQUALS":
|
|
265
|
+
self.next()
|
|
266
|
+
dt = self.next()
|
|
267
|
+
if dt.kind == "NUMBER":
|
|
268
|
+
default = int(dt.value) if "." not in dt.value else float(dt.value)
|
|
269
|
+
elif dt.kind == "STRING":
|
|
270
|
+
default = dt.value
|
|
271
|
+
elif dt.kind == "IDENT":
|
|
272
|
+
if dt.value == "true":
|
|
273
|
+
default = True
|
|
274
|
+
elif dt.value == "false":
|
|
275
|
+
default = False
|
|
276
|
+
else:
|
|
277
|
+
default = dt.value
|
|
278
|
+
return StateVar(name=name_tok.value, type=type_tok.value, default=default)
|
|
279
|
+
|
|
280
|
+
def parse_widget(self) -> WidgetNode:
|
|
281
|
+
"""Parse a widget definition."""
|
|
282
|
+
kind_tok = self.expect("IDENT")
|
|
283
|
+
node = WidgetNode(kind=kind_tok.value)
|
|
284
|
+
|
|
285
|
+
# handle text/label directly after widget name: Text "Hello" or Button "Click"
|
|
286
|
+
if self.peek().kind == "STRING":
|
|
287
|
+
text_tok = self.next()
|
|
288
|
+
if kind_tok.value == "Text":
|
|
289
|
+
node.text = text_tok.value
|
|
290
|
+
elif kind_tok.value in ("ElevatedButton", "TextButton"):
|
|
291
|
+
node.label = text_tok.value
|
|
292
|
+
else:
|
|
293
|
+
node.props["text"] = text_tok.value
|
|
294
|
+
|
|
295
|
+
# parse properties until we see children or RBRACE
|
|
296
|
+
while self.peek().kind == "IDENT":
|
|
297
|
+
prop_tok = self.peek()
|
|
298
|
+
if prop_tok.value in ALL_WIDGETS:
|
|
299
|
+
# this is a child widget, not a property
|
|
300
|
+
break
|
|
301
|
+
if prop_tok.value == "children":
|
|
302
|
+
self.next()
|
|
303
|
+
self.expect("COLON")
|
|
304
|
+
while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
|
|
305
|
+
child = self.parse_widget()
|
|
306
|
+
node.children.append(child)
|
|
307
|
+
break
|
|
308
|
+
# parse property: name = value or name: value
|
|
309
|
+
self.next() # consume property name
|
|
310
|
+
if self.peek().kind == "COLON":
|
|
311
|
+
self.next()
|
|
312
|
+
elif self.peek().kind == "EQUALS":
|
|
313
|
+
self.next()
|
|
314
|
+
else:
|
|
315
|
+
# property without value (e.g., "Divider")
|
|
316
|
+
node.props[prop_tok.value] = True
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
val_tok = self.next()
|
|
320
|
+
if val_tok.kind == "STRING":
|
|
321
|
+
if prop_tok.value == "style":
|
|
322
|
+
from .styling import parse_style
|
|
323
|
+
node.style_dict = parse_style(val_tok.value)
|
|
324
|
+
elif prop_tok.value == "state":
|
|
325
|
+
node.state = val_tok.value
|
|
326
|
+
elif prop_tok.value == "text":
|
|
327
|
+
node.text = val_tok.value
|
|
328
|
+
elif prop_tok.value == "label":
|
|
329
|
+
node.label = val_tok.value
|
|
330
|
+
elif prop_tok.value == "on_click":
|
|
331
|
+
node.action = {"call": val_tok.value}
|
|
332
|
+
else:
|
|
333
|
+
node.props[prop_tok.value] = val_tok.value
|
|
334
|
+
elif val_tok.kind == "NUMBER":
|
|
335
|
+
if "." in val_tok.value:
|
|
336
|
+
node.props[prop_tok.value] = float(val_tok.value)
|
|
337
|
+
else:
|
|
338
|
+
node.props[prop_tok.value] = int(val_tok.value)
|
|
339
|
+
elif val_tok.kind == "IDENT":
|
|
340
|
+
if val_tok.value == "Action":
|
|
341
|
+
# parse Action(call="fn", args=[...], update="state")
|
|
342
|
+
node.action = self.parse_action()
|
|
343
|
+
elif val_tok.value in ("true", "false"):
|
|
344
|
+
node.props[prop_tok.value] = (val_tok.value == "true")
|
|
345
|
+
else:
|
|
346
|
+
node.props[prop_tok.value] = val_tok.value
|
|
347
|
+
|
|
348
|
+
# parse body block if present
|
|
349
|
+
if self.peek().kind == "LBRACE":
|
|
350
|
+
self.next() # consume {
|
|
351
|
+
while self.peek().kind != "RBRACE":
|
|
352
|
+
if self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
|
|
353
|
+
child = self.parse_widget()
|
|
354
|
+
node.children.append(child)
|
|
355
|
+
elif self.peek().kind == "IDENT" and self.peek().value == "children":
|
|
356
|
+
self.next()
|
|
357
|
+
self.expect("COLON")
|
|
358
|
+
while self.peek().kind == "IDENT" and self.peek().value in ALL_WIDGETS:
|
|
359
|
+
child = self.parse_widget()
|
|
360
|
+
node.children.append(child)
|
|
361
|
+
elif self.peek().kind == "STRING":
|
|
362
|
+
# bare string in body — treat as Text widget
|
|
363
|
+
text_tok = self.next()
|
|
364
|
+
child = WidgetNode(kind="Text", text=text_tok.value)
|
|
365
|
+
node.children.append(child)
|
|
366
|
+
elif self.peek().kind == "IDENT":
|
|
367
|
+
# property inside body block: name: value or name = value
|
|
368
|
+
prop_tok = self.next()
|
|
369
|
+
if self.peek().kind == "COLON":
|
|
370
|
+
self.next()
|
|
371
|
+
elif self.peek().kind == "EQUALS":
|
|
372
|
+
self.next()
|
|
373
|
+
else:
|
|
374
|
+
node.props[prop_tok.value] = True
|
|
375
|
+
continue
|
|
376
|
+
val_tok = self.next()
|
|
377
|
+
if val_tok.kind == "STRING":
|
|
378
|
+
if prop_tok.value == "style":
|
|
379
|
+
from .styling import parse_style
|
|
380
|
+
node.style_dict = parse_style(val_tok.value)
|
|
381
|
+
elif prop_tok.value == "state":
|
|
382
|
+
node.state = val_tok.value
|
|
383
|
+
elif prop_tok.value == "text":
|
|
384
|
+
node.text = val_tok.value
|
|
385
|
+
elif prop_tok.value == "label":
|
|
386
|
+
node.label = val_tok.value
|
|
387
|
+
elif prop_tok.value == "on_click":
|
|
388
|
+
node.action = {"call": val_tok.value}
|
|
389
|
+
else:
|
|
390
|
+
node.props[prop_tok.value] = val_tok.value
|
|
391
|
+
elif val_tok.kind == "NUMBER":
|
|
392
|
+
if "." in val_tok.value:
|
|
393
|
+
node.props[prop_tok.value] = float(val_tok.value)
|
|
394
|
+
else:
|
|
395
|
+
node.props[prop_tok.value] = int(val_tok.value)
|
|
396
|
+
elif val_tok.kind == "IDENT":
|
|
397
|
+
if val_tok.value == "Action":
|
|
398
|
+
node.action = self.parse_action()
|
|
399
|
+
elif val_tok.value in ("true", "false"):
|
|
400
|
+
node.props[prop_tok.value] = (val_tok.value == "true")
|
|
401
|
+
else:
|
|
402
|
+
node.props[prop_tok.value] = val_tok.value
|
|
403
|
+
else:
|
|
404
|
+
raise SyntaxError(f"Unexpected token in widget body: {self.peek().value!r}")
|
|
405
|
+
self.expect("RBRACE")
|
|
406
|
+
|
|
407
|
+
return node
|
|
408
|
+
|
|
409
|
+
def parse_action(self) -> dict[str, Any]:
|
|
410
|
+
"""Parse an Action(...) expression."""
|
|
411
|
+
self.expect("LPAREN")
|
|
412
|
+
action: dict[str, Any] = {"args": []}
|
|
413
|
+
while self.peek().kind != "RPAREN":
|
|
414
|
+
prop = self.expect("IDENT")
|
|
415
|
+
self.expect("EQUALS")
|
|
416
|
+
val = self.next()
|
|
417
|
+
if val.kind == "STRING":
|
|
418
|
+
action[prop.value] = val.value
|
|
419
|
+
elif val.kind == "NUMBER":
|
|
420
|
+
action[prop.value] = int(val.value) if "." not in val.value else float(val.value)
|
|
421
|
+
elif val.kind == "LBRACKET":
|
|
422
|
+
# parse array
|
|
423
|
+
while self.peek().kind != "RBRACKET":
|
|
424
|
+
v = self.next()
|
|
425
|
+
if v.kind == "STRING":
|
|
426
|
+
action["args"].append(v.value)
|
|
427
|
+
elif v.kind == "NUMBER":
|
|
428
|
+
action["args"].append(int(v.value) if "." not in v.value else float(v.value))
|
|
429
|
+
if self.peek().kind == "COMMA":
|
|
430
|
+
self.next()
|
|
431
|
+
self.expect("RBRACKET")
|
|
432
|
+
if self.peek().kind == "COMMA":
|
|
433
|
+
self.next()
|
|
434
|
+
self.expect("RPAREN")
|
|
435
|
+
return action
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def parse_ui_dsl(source: str) -> Screen:
|
|
439
|
+
"""Parse a .ge.ui file and return a Screen definition."""
|
|
440
|
+
tokens = tokenize(source)
|
|
441
|
+
parser = Parser(tokens)
|
|
442
|
+
return parser.parse_screen()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def parse_ui_file(path) -> Screen:
|
|
446
|
+
"""Parse a .ge.ui file from disk."""
|
|
447
|
+
from pathlib import Path
|
|
448
|
+
p = Path(path)
|
|
449
|
+
source = p.read_text(encoding="utf-8")
|
|
450
|
+
return parse_ui_dsl(source)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def collect_state_from_screen(screen: Screen) -> dict[str, str]:
|
|
454
|
+
"""Collect all state variables referenced in the screen."""
|
|
455
|
+
states: dict[str, str] = {}
|
|
456
|
+
for sv in screen.state_vars:
|
|
457
|
+
states[sv.name] = sv.type
|
|
458
|
+
if screen.root:
|
|
459
|
+
_collect_state_recursive(screen.root, states)
|
|
460
|
+
return states
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _collect_state_recursive(node: WidgetNode, states: dict[str, str]) -> None:
|
|
464
|
+
if node.state:
|
|
465
|
+
states[node.state] = "int" # default type
|
|
466
|
+
if node.action and "update" in node.action:
|
|
467
|
+
states[node.action["update"]] = "int"
|
|
468
|
+
for child in node.children:
|
|
469
|
+
_collect_state_recursive(child, states)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---- Flutter code generation from UI DSL ----
|
|
473
|
+
|
|
474
|
+
def ui_dsl_to_flutter(screen: Screen) -> str:
|
|
475
|
+
"""Convert a parsed UI DSL Screen to Flutter Dart code.
|
|
476
|
+
|
|
477
|
+
This generates a complete Flutter widget that can be merged into
|
|
478
|
+
main.dart alongside the existing UI generation.
|
|
479
|
+
"""
|
|
480
|
+
lines: list[str] = []
|
|
481
|
+
lines.append("// AUTO-GENERATED from GE UI DSL — do not edit by hand.")
|
|
482
|
+
lines.append("import 'package:flutter/material.dart';")
|
|
483
|
+
lines.append("import 'bindings.dart' as ffi;")
|
|
484
|
+
lines.append("")
|
|
485
|
+
|
|
486
|
+
# generate state class
|
|
487
|
+
class_name = screen.title.replace(" ", "") + "Screen"
|
|
488
|
+
lines.append(f"class {class_name} extends StatefulWidget {{")
|
|
489
|
+
lines.append(f" const {class_name}({{super.key}});")
|
|
490
|
+
lines.append(f" @override")
|
|
491
|
+
lines.append(f" State<{class_name}> createState() => _{class_name}State();")
|
|
492
|
+
lines.append(f"}}")
|
|
493
|
+
lines.append("")
|
|
494
|
+
|
|
495
|
+
# generate state class body
|
|
496
|
+
lines.append(f"class _{class_name}State extends State<{class_name}> {{")
|
|
497
|
+
for sv in screen.state_vars:
|
|
498
|
+
dart_type = _dart_type(sv.type)
|
|
499
|
+
default_val = _dart_default(sv.default, sv.type)
|
|
500
|
+
lines.append(f" {dart_type} {sv.name} = {default_val};")
|
|
501
|
+
lines.append("")
|
|
502
|
+
|
|
503
|
+
lines.append(" @override")
|
|
504
|
+
lines.append(" Widget build(BuildContext context) {")
|
|
505
|
+
lines.append(" return Scaffold(")
|
|
506
|
+
lines.append(f" appBar: AppBar(title: const Text('{screen.title}')),")
|
|
507
|
+
lines.append(" body: " + _widget_to_flutter(screen.root, 8))
|
|
508
|
+
lines.append(" );")
|
|
509
|
+
lines.append(" }")
|
|
510
|
+
lines.append("}")
|
|
511
|
+
|
|
512
|
+
return "\n".join(lines) + "\n"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _dart_type(ge_type: str) -> str:
|
|
516
|
+
return {"int": "int", "float": "double", "bool": "bool",
|
|
517
|
+
"str": "String", "string": "String"}.get(ge_type, "int")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _dart_default(val: Any, ge_type: str) -> str:
|
|
521
|
+
if val is None:
|
|
522
|
+
return {"int": "0", "float": "0.0", "bool": "false",
|
|
523
|
+
"str": "''", "string": "''"}.get(ge_type, "0")
|
|
524
|
+
if isinstance(val, str):
|
|
525
|
+
return f"'{val}'"
|
|
526
|
+
return str(val)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _widget_to_flutter(node: WidgetNode | None, indent: int) -> str:
|
|
530
|
+
"""Convert a widget node to Flutter Dart code."""
|
|
531
|
+
if node is None:
|
|
532
|
+
return "const SizedBox.shrink()"
|
|
533
|
+
ind = " " * indent
|
|
534
|
+
kind = node.kind
|
|
535
|
+
|
|
536
|
+
if kind == "Column":
|
|
537
|
+
children = ",\n".join(_widget_to_flutter(c, indent + 6) for c in node.children)
|
|
538
|
+
return f"Column(\n{ind} crossAxisAlignment: CrossAxisAlignment.start,\n{ind} children: [\n{children},\n{ind} ],\n{ind})"
|
|
539
|
+
elif kind == "Row":
|
|
540
|
+
children = ",\n".join(_widget_to_flutter(c, indent + 6) for c in node.children)
|
|
541
|
+
return f"Row(\n{ind} children: [\n{children},\n{ind} ],\n{ind})"
|
|
542
|
+
elif kind == "Text":
|
|
543
|
+
if node.state:
|
|
544
|
+
return f"Text('$\\{{{node.state}\\}}', style: {_style_to_flutter(node.style_dict)})"
|
|
545
|
+
elif node.text:
|
|
546
|
+
return f"Text('{node.text}', style: {_style_to_flutter(node.style_dict)})"
|
|
547
|
+
return "Text('')"
|
|
548
|
+
elif kind == "Divider":
|
|
549
|
+
return "const Divider()"
|
|
550
|
+
elif kind == "SizedBox":
|
|
551
|
+
h = node.props.get("height", "")
|
|
552
|
+
w = node.props.get("width", "")
|
|
553
|
+
if h and w:
|
|
554
|
+
return f"const SizedBox(height: {h}, width: {w})"
|
|
555
|
+
elif h:
|
|
556
|
+
return f"const SizedBox(height: {h})"
|
|
557
|
+
elif w:
|
|
558
|
+
return f"const SizedBox(width: {w})"
|
|
559
|
+
return "const SizedBox.shrink()"
|
|
560
|
+
elif kind == "ElevatedButton":
|
|
561
|
+
label = node.label or node.text or "Click"
|
|
562
|
+
if node.action:
|
|
563
|
+
call = node.action.get("call", "")
|
|
564
|
+
update = node.action.get("update", "")
|
|
565
|
+
if update:
|
|
566
|
+
return f"ElevatedButton(onPressed: () {{ setState(() {{ {update} = ffi.{call}(); }}); }}, child: const Text('{label}'))"
|
|
567
|
+
else:
|
|
568
|
+
return f"ElevatedButton(onPressed: () {{ ffi.{call}(); }}, child: const Text('{label}'))"
|
|
569
|
+
return f"ElevatedButton(onPressed: () {{}}, child: const Text('{label}'))"
|
|
570
|
+
elif kind == "Container":
|
|
571
|
+
padding = node.style_dict.get("padding", 0)
|
|
572
|
+
child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
|
|
573
|
+
return f"Container(padding: const EdgeInsets.all({padding}), child: {child})"
|
|
574
|
+
elif kind == "Expanded":
|
|
575
|
+
child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
|
|
576
|
+
return f"Expanded(child: {child})"
|
|
577
|
+
elif kind == "Card":
|
|
578
|
+
child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
|
|
579
|
+
return f"Card(child: {child})"
|
|
580
|
+
elif kind == "Center":
|
|
581
|
+
child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
|
|
582
|
+
return f"Center(child: {child})"
|
|
583
|
+
elif kind == "Padding":
|
|
584
|
+
padding = node.props.get("padding", 16)
|
|
585
|
+
child = _widget_to_flutter(node.children[0] if node.children else None, indent + 2)
|
|
586
|
+
return f"Padding(padding: const EdgeInsets.all({padding}), child: {child})"
|
|
587
|
+
else:
|
|
588
|
+
return f"// Unknown widget: {kind}"
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _style_to_flutter(style_dict: dict[str, Any]) -> str:
|
|
592
|
+
"""Convert a style dict to a Flutter TextStyle."""
|
|
593
|
+
if not style_dict:
|
|
594
|
+
return "const TextStyle()"
|
|
595
|
+
parts = []
|
|
596
|
+
if "fontSize" in style_dict:
|
|
597
|
+
parts.append(f"fontSize: {style_dict['fontSize']}")
|
|
598
|
+
if style_dict.get("bold"):
|
|
599
|
+
parts.append("fontWeight: FontWeight.bold")
|
|
600
|
+
if "color" in style_dict:
|
|
601
|
+
color = style_dict["color"]
|
|
602
|
+
if color.startswith("#"):
|
|
603
|
+
parts.append(f"color: const Color(0xFF{color[1:]})")
|
|
604
|
+
else:
|
|
605
|
+
parts.append(f"color: Colors.{_map_color(color)}")
|
|
606
|
+
if not parts:
|
|
607
|
+
return "const TextStyle()"
|
|
608
|
+
return f"TextStyle({', '.join(parts)})"
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _map_color(color: str) -> str:
|
|
612
|
+
"""Map a named color to a Flutter Colors name."""
|
|
613
|
+
mapping = {
|
|
614
|
+
"red": "red", "blue": "blue", "green": "green",
|
|
615
|
+
"black": "black", "white": "white", "grey": "grey",
|
|
616
|
+
"orange": "orange", "purple": "purple", "teal": "teal",
|
|
617
|
+
}
|
|
618
|
+
return mapping.get(color, "black")
|