uigen 0.1.0__py3-none-any.whl
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.
- uigen/__init__.py +19 -0
- uigen/_cext.c +79 -0
- uigen/accel.py +60 -0
- uigen/cli.py +139 -0
- uigen/core/__init__.py +7 -0
- uigen/core/api.py +128 -0
- uigen/core/compiler.py +212 -0
- uigen/core/components.py +178 -0
- uigen/core/schema.py +88 -0
- uigen/renderers/__init__.py +6 -0
- uigen/renderers/base.py +47 -0
- uigen/renderers/ldjango.py +384 -0
- uigen/renderers/lflask.py +313 -0
- uigen/renderers/lnative.py +251 -0
- uigen/renderers/lreact.py +317 -0
- uigen/theme.py +228 -0
- uigen-0.1.0.dist-info/METADATA +453 -0
- uigen-0.1.0.dist-info/RECORD +22 -0
- uigen-0.1.0.dist-info/WHEEL +5 -0
- uigen-0.1.0.dist-info/entry_points.txt +2 -0
- uigen-0.1.0.dist-info/licenses/LICENSE +21 -0
- uigen-0.1.0.dist-info/top_level.txt +1 -0
uigen/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""uigen — Write UI logic once, deploy anywhere."""
|
|
2
|
+
|
|
3
|
+
from uigen.core.api import ui
|
|
4
|
+
from uigen.core.compiler import App
|
|
5
|
+
from uigen.core.schema import Field, Model
|
|
6
|
+
from uigen.theme import Theme, get_theme, list_themes, register_theme
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Model",
|
|
12
|
+
"Field",
|
|
13
|
+
"ui",
|
|
14
|
+
"App",
|
|
15
|
+
"Theme",
|
|
16
|
+
"get_theme",
|
|
17
|
+
"list_themes",
|
|
18
|
+
"register_theme",
|
|
19
|
+
]
|
uigen/_cext.c
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uigen C extension — fast HTML escaping
|
|
3
|
+
*
|
|
4
|
+
* This module provides C-accelerated HTML entity escaping.
|
|
5
|
+
*
|
|
6
|
+
* Build with: python setup.py build_ext --inplace
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#define PY_SSIZE_T_CLEAN
|
|
10
|
+
#include <Python.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
#include <stdlib.h>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fast HTML entity escaping
|
|
16
|
+
*/
|
|
17
|
+
static PyObject* escape_html(PyObject* self, PyObject* args) {
|
|
18
|
+
const char* input;
|
|
19
|
+
Py_ssize_t input_len;
|
|
20
|
+
|
|
21
|
+
if (!PyArg_ParseTuple(args, "s#", &input, &input_len)) {
|
|
22
|
+
return NULL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Allocate output buffer (worst case: every char needs escaping) */
|
|
26
|
+
char* output = (char*)malloc(input_len * 7 + 1);
|
|
27
|
+
if (!output) {
|
|
28
|
+
return PyErr_NoMemory();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
char* out = output;
|
|
32
|
+
for (Py_ssize_t i = 0; i < input_len; i++) {
|
|
33
|
+
char c = input[i];
|
|
34
|
+
switch (c) {
|
|
35
|
+
case '&': memcpy(out, "&", 5); out += 5; break;
|
|
36
|
+
case '<': memcpy(out, "<", 4); out += 4; break;
|
|
37
|
+
case '>': memcpy(out, ">", 4); out += 4; break;
|
|
38
|
+
case '"': memcpy(out, """, 6); out += 6; break;
|
|
39
|
+
case '\'': memcpy(out, "'", 5); out += 5; break;
|
|
40
|
+
default: *out++ = c; break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
*out = '\0';
|
|
44
|
+
|
|
45
|
+
PyObject* result = PyUnicode_FromString(output);
|
|
46
|
+
free(output);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Module method definitions
|
|
52
|
+
*/
|
|
53
|
+
static PyMethodDef uigen_methods[] = {
|
|
54
|
+
{
|
|
55
|
+
"escape_html",
|
|
56
|
+
escape_html,
|
|
57
|
+
METH_VARARGS,
|
|
58
|
+
"Escape HTML special characters"
|
|
59
|
+
},
|
|
60
|
+
{NULL, NULL, 0, NULL}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Module definition
|
|
65
|
+
*/
|
|
66
|
+
static struct PyModuleDef uigen_module = {
|
|
67
|
+
PyModuleDef_HEAD_INIT,
|
|
68
|
+
"uigen._cext",
|
|
69
|
+
"uigen C extension for fast HTML escaping",
|
|
70
|
+
-1,
|
|
71
|
+
uigen_methods
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Module initialization
|
|
76
|
+
*/
|
|
77
|
+
PyMODINIT_FUNC PyInit__cext(void) {
|
|
78
|
+
return PyModule_Create(&uigen_module);
|
|
79
|
+
}
|
uigen/accel.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Accelerated functions using C extension (optional)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _check_cext() -> bool:
|
|
9
|
+
"""Check if C extension is available."""
|
|
10
|
+
try:
|
|
11
|
+
import uigen._cext # noqa: F401
|
|
12
|
+
return True
|
|
13
|
+
except ImportError:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
HAS_CEXT = _check_cext()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def escape_html(text: str) -> str:
|
|
21
|
+
"""Escape HTML special characters.
|
|
22
|
+
|
|
23
|
+
Uses C extension if available, falls back to pure Python.
|
|
24
|
+
"""
|
|
25
|
+
if HAS_CEXT:
|
|
26
|
+
from uigen._cext import escape_html as _escape_html
|
|
27
|
+
return _escape_html(text)
|
|
28
|
+
|
|
29
|
+
# Pure Python fallback
|
|
30
|
+
replacements = {
|
|
31
|
+
"&": "&",
|
|
32
|
+
"<": "<",
|
|
33
|
+
">": ">",
|
|
34
|
+
'"': """,
|
|
35
|
+
"'": "'",
|
|
36
|
+
}
|
|
37
|
+
for char, escaped in replacements.items():
|
|
38
|
+
text = text.replace(char, escaped)
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def concat_strings(parts: list[str]) -> str:
|
|
43
|
+
"""Fast string concatenation.
|
|
44
|
+
|
|
45
|
+
Uses C extension if available, falls back to pure Python.
|
|
46
|
+
"""
|
|
47
|
+
if HAS_CEXT:
|
|
48
|
+
from uigen._cext import concat_strings as _concat
|
|
49
|
+
return _concat(parts)
|
|
50
|
+
|
|
51
|
+
# Pure Python fallback
|
|
52
|
+
return "".join(parts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_info() -> dict[str, Any]:
|
|
56
|
+
"""Get information about the C extension."""
|
|
57
|
+
return {
|
|
58
|
+
"has_cext": HAS_CEXT,
|
|
59
|
+
"backend": "C" if HAS_CEXT else "Python",
|
|
60
|
+
}
|
uigen/cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CLI — command-line interface for uigen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> int:
|
|
11
|
+
"""Main entry point for the uigen CLI."""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="uigen",
|
|
14
|
+
description="Write UI logic once, deploy anywhere.",
|
|
15
|
+
)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
17
|
+
|
|
18
|
+
# uigen init
|
|
19
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new uigen project")
|
|
20
|
+
init_parser.add_argument("name", help="Project name")
|
|
21
|
+
|
|
22
|
+
# uigen generate
|
|
23
|
+
gen_parser = subparsers.add_parser("generate", help="Generate frontend code")
|
|
24
|
+
gen_parser.add_argument(
|
|
25
|
+
"--renderer", "-r",
|
|
26
|
+
default="lnative",
|
|
27
|
+
choices=["lnative", "lreact", "lflask", "ldjango"],
|
|
28
|
+
help="Renderer to use (default: lnative)",
|
|
29
|
+
)
|
|
30
|
+
gen_parser.add_argument(
|
|
31
|
+
"--output", "-o",
|
|
32
|
+
default="./dist",
|
|
33
|
+
help="Output directory (default: ./dist)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# uigen renderers
|
|
37
|
+
subparsers.add_parser("renderers", help="List available renderers")
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args(argv)
|
|
40
|
+
|
|
41
|
+
if args.command == "init":
|
|
42
|
+
return _init_project(args.name)
|
|
43
|
+
elif args.command == "generate":
|
|
44
|
+
return _generate(args.renderer, args.output)
|
|
45
|
+
elif args.command == "renderers":
|
|
46
|
+
_list_renderers()
|
|
47
|
+
return 0
|
|
48
|
+
else:
|
|
49
|
+
parser.print_help()
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _init_project(name: str) -> int:
|
|
54
|
+
"""Initialize a new uigen project."""
|
|
55
|
+
project_dir = Path(name)
|
|
56
|
+
|
|
57
|
+
if project_dir.exists():
|
|
58
|
+
print(f"Error: Directory '{name}' already exists.", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
project_dir.mkdir(parents=True)
|
|
62
|
+
|
|
63
|
+
# Create main.py
|
|
64
|
+
main_file = project_dir / "main.py"
|
|
65
|
+
main_file.write_text(f'''"""Generated by uigen — {name}"""
|
|
66
|
+
|
|
67
|
+
from uigen import App, Model, ui
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class User(Model):
|
|
71
|
+
name: str
|
|
72
|
+
email: str
|
|
73
|
+
role: str = "viewer"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Build your UI here
|
|
77
|
+
page = ui.page(
|
|
78
|
+
ui.card(
|
|
79
|
+
ui.heading("Welcome to {name}"),
|
|
80
|
+
ui.text("Edit main.py to customize this page."),
|
|
81
|
+
ui.button("Get Started"),
|
|
82
|
+
),
|
|
83
|
+
title="{name}",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
app = App(title="{name}", pages=[page])
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
output = app.render("lnative", output="./dist")
|
|
90
|
+
print(f"Generated UI at {{output}}")
|
|
91
|
+
''')
|
|
92
|
+
|
|
93
|
+
print(f"Created project: {name}")
|
|
94
|
+
print(f" cd {name}")
|
|
95
|
+
print(" python main.py")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate(renderer: str, output: str) -> int:
|
|
100
|
+
"""Generate frontend from main.py."""
|
|
101
|
+
main_file = Path("main.py")
|
|
102
|
+
if not main_file.exists():
|
|
103
|
+
print("Error: No main.py found. Run 'uigen init' first.", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
# Execute main.py to get the app object
|
|
107
|
+
import importlib.util
|
|
108
|
+
spec = importlib.util.spec_from_file_location("main", main_file)
|
|
109
|
+
if spec is None or spec.loader is None:
|
|
110
|
+
print("Error: Could not load main.py", file=sys.stderr)
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
module = importlib.util.module_from_spec(spec)
|
|
114
|
+
spec.loader.exec_module(module)
|
|
115
|
+
|
|
116
|
+
if not hasattr(module, "app"):
|
|
117
|
+
print(
|
|
118
|
+
"Error: main.py must define an 'app' variable (App instance)",
|
|
119
|
+
file=sys.stderr,
|
|
120
|
+
)
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
app = module.app
|
|
124
|
+
output_path = app.render(renderer, output=output)
|
|
125
|
+
print(f"Generated {renderer} output at {output_path}")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _list_renderers() -> None:
|
|
130
|
+
"""List available renderers."""
|
|
131
|
+
print("Available renderers:")
|
|
132
|
+
print(" lnative - Static HTML/CSS/JS (ready)")
|
|
133
|
+
print(" lreact - React components (coming soon)")
|
|
134
|
+
print(" lflask - Flask/Jinja2 templates (coming soon)")
|
|
135
|
+
print(" ldjango - Django templates (coming soon)")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
sys.exit(main())
|
uigen/core/__init__.py
ADDED
uigen/core/api.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Public API — the ui object that users interact with."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from uigen.core.components import (
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
Component,
|
|
11
|
+
Form,
|
|
12
|
+
Grid,
|
|
13
|
+
Heading,
|
|
14
|
+
Input,
|
|
15
|
+
Modal,
|
|
16
|
+
Page,
|
|
17
|
+
Select,
|
|
18
|
+
Stack,
|
|
19
|
+
Table,
|
|
20
|
+
Text,
|
|
21
|
+
)
|
|
22
|
+
from uigen.core.schema import Model
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UI:
|
|
26
|
+
"""The ui namespace — provides methods to create UI components."""
|
|
27
|
+
|
|
28
|
+
def page(
|
|
29
|
+
self, *children: Component, title: str = "", description: str = ""
|
|
30
|
+
) -> Page:
|
|
31
|
+
"""Create a page container."""
|
|
32
|
+
return Page(
|
|
33
|
+
children=list(children),
|
|
34
|
+
title=title,
|
|
35
|
+
description=description,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def card(self, *children: Component, **attrs: Any) -> Card:
|
|
39
|
+
"""Create a card container."""
|
|
40
|
+
return Card(children=list(children), attrs=attrs)
|
|
41
|
+
|
|
42
|
+
def heading(self, text: str, level: int = 1) -> Heading:
|
|
43
|
+
"""Create a heading element."""
|
|
44
|
+
return Heading(text=text, level=level)
|
|
45
|
+
|
|
46
|
+
def text(self, content: str) -> Text:
|
|
47
|
+
"""Create a paragraph/text element."""
|
|
48
|
+
return Text(text=content)
|
|
49
|
+
|
|
50
|
+
def button(self, text: str, variant: str = "primary", onclick: str = "") -> Button:
|
|
51
|
+
"""Create a button element."""
|
|
52
|
+
return Button(text=text, variant=variant, onclick=onclick)
|
|
53
|
+
|
|
54
|
+
def input(
|
|
55
|
+
self,
|
|
56
|
+
name: str,
|
|
57
|
+
label: str = "",
|
|
58
|
+
type: str = "text",
|
|
59
|
+
placeholder: str = "",
|
|
60
|
+
value: str = "",
|
|
61
|
+
required: bool = False,
|
|
62
|
+
) -> Input:
|
|
63
|
+
"""Create a form input."""
|
|
64
|
+
return Input(
|
|
65
|
+
name=name,
|
|
66
|
+
label=label or name.replace("_", " ").title(),
|
|
67
|
+
input_type=type,
|
|
68
|
+
placeholder=placeholder,
|
|
69
|
+
value=value,
|
|
70
|
+
required=required,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def select(
|
|
74
|
+
self, name: str, options: list[str], label: str = "", value: str = ""
|
|
75
|
+
) -> Select:
|
|
76
|
+
"""Create a select dropdown."""
|
|
77
|
+
return Select(
|
|
78
|
+
name=name,
|
|
79
|
+
options=options,
|
|
80
|
+
label=label or name.replace("_", " ").title(),
|
|
81
|
+
value=value,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def table(
|
|
85
|
+
self,
|
|
86
|
+
data: list[dict[str, Any]] | Model | None = None,
|
|
87
|
+
columns: list[str] | None = None,
|
|
88
|
+
) -> Table:
|
|
89
|
+
"""Create a data table.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: List of dicts, a Model class (uses its fields), or None.
|
|
93
|
+
columns: Column names. If None, auto-detected from data.
|
|
94
|
+
"""
|
|
95
|
+
rows: list[dict[str, Any]] = []
|
|
96
|
+
cols = columns or []
|
|
97
|
+
|
|
98
|
+
if data is None:
|
|
99
|
+
cols = cols or []
|
|
100
|
+
elif isinstance(data, type) and issubclass(data, Model):
|
|
101
|
+
cols = cols or data.field_names()
|
|
102
|
+
elif isinstance(data, list):
|
|
103
|
+
rows = data
|
|
104
|
+
if not cols and rows:
|
|
105
|
+
cols = list(rows[0].keys())
|
|
106
|
+
|
|
107
|
+
return Table(columns=cols, rows=rows)
|
|
108
|
+
|
|
109
|
+
def form(
|
|
110
|
+
self, *children: Component, action: str = "", method: str = "POST"
|
|
111
|
+
) -> Form:
|
|
112
|
+
"""Create a form container."""
|
|
113
|
+
return Form(children=list(children), action=action, method=method)
|
|
114
|
+
|
|
115
|
+
def modal(self, *children: Component, title: str = "") -> Modal:
|
|
116
|
+
"""Create a modal dialog."""
|
|
117
|
+
return Modal(children=list(children), title=title)
|
|
118
|
+
|
|
119
|
+
def grid(self, *children: Component, columns: int = 2) -> Grid:
|
|
120
|
+
"""Create a grid layout."""
|
|
121
|
+
return Grid(children=list(children), columns=columns)
|
|
122
|
+
|
|
123
|
+
def stack(self, *children: Component, spacing: str = "md") -> Stack:
|
|
124
|
+
"""Create a vertical stack layout."""
|
|
125
|
+
return Stack(children=list(children), spacing=spacing)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
ui = UI()
|
uigen/core/compiler.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Compiler — transforms UI definitions into renderer output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from uigen.core.components import Page
|
|
9
|
+
from uigen.core.schema import Model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class App:
|
|
13
|
+
"""Main application class that wires models and pages to renderers."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
title: str = "My App",
|
|
18
|
+
models: list[type[Model]] | None = None,
|
|
19
|
+
pages: list[Page] | None = None,
|
|
20
|
+
theme: str | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.title = title
|
|
23
|
+
self.models = models or []
|
|
24
|
+
self.pages = pages or []
|
|
25
|
+
self.theme = theme
|
|
26
|
+
self._renderer: Any = None
|
|
27
|
+
|
|
28
|
+
def add_page(self, page: Page) -> App:
|
|
29
|
+
"""Add a page to the app."""
|
|
30
|
+
self.pages.append(page)
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def render(self, renderer_name: str, output: str = "./dist") -> Path:
|
|
34
|
+
"""Render all pages using the specified renderer.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
renderer_name: Name of the renderer (e.g., "lnative", "lreact").
|
|
38
|
+
output: Output directory path.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to the output directory.
|
|
42
|
+
"""
|
|
43
|
+
renderer = self._get_renderer(renderer_name)
|
|
44
|
+
output_path = Path(output)
|
|
45
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
if renderer_name == "lreact":
|
|
48
|
+
return self._render_react(renderer, output_path)
|
|
49
|
+
elif renderer_name == "lflask":
|
|
50
|
+
return self._render_flask(renderer, output_path)
|
|
51
|
+
elif renderer_name == "ldjango":
|
|
52
|
+
return self._render_django(renderer, output_path)
|
|
53
|
+
else:
|
|
54
|
+
return self._render_native(renderer, output_path)
|
|
55
|
+
|
|
56
|
+
def _render_native(self, renderer: Any, output_path: Path) -> Path:
|
|
57
|
+
"""Render pages as static HTML files."""
|
|
58
|
+
for i, page in enumerate(self.pages):
|
|
59
|
+
filename = "index.html" if i == 0 else f"page_{i}.html"
|
|
60
|
+
content = renderer.render_page(page, title=self.title)
|
|
61
|
+
(output_path / filename).write_text(content)
|
|
62
|
+
return output_path
|
|
63
|
+
|
|
64
|
+
def _render_react(self, renderer: Any, output_path: Path) -> Path:
|
|
65
|
+
"""Render pages as a React project."""
|
|
66
|
+
components_dir = output_path / "src" / "components"
|
|
67
|
+
components_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
component_names = []
|
|
70
|
+
for i, page in enumerate(self.pages):
|
|
71
|
+
page_title = page.title or f"Page{i}"
|
|
72
|
+
component_name = renderer._to_pascal_case(page_title)
|
|
73
|
+
component_names.append(component_name)
|
|
74
|
+
|
|
75
|
+
content = renderer.render_page(page, title=self.title)
|
|
76
|
+
(components_dir / f"{component_name}.tsx").write_text(content)
|
|
77
|
+
|
|
78
|
+
# Generate package.json
|
|
79
|
+
(output_path / "package.json").write_text(
|
|
80
|
+
renderer.generate_package_json(self.title)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Generate index.html
|
|
84
|
+
(output_path / "index.html").write_text(
|
|
85
|
+
renderer.generate_index_html(self.title)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Generate App.tsx
|
|
89
|
+
(output_path / "src" / "App.tsx").write_text(
|
|
90
|
+
renderer.generate_app_tsx(component_names)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Generate index.tsx
|
|
94
|
+
(output_path / "src" / "index.tsx").write_text(
|
|
95
|
+
"""import React from 'react';
|
|
96
|
+
import ReactDOM from 'react-dom/client';
|
|
97
|
+
import App from './App';
|
|
98
|
+
|
|
99
|
+
const root = ReactDOM.createRoot(
|
|
100
|
+
document.getElementById('root') as HTMLElement
|
|
101
|
+
);
|
|
102
|
+
root.render(
|
|
103
|
+
<React.StrictMode>
|
|
104
|
+
<App />
|
|
105
|
+
</React.StrictMode>
|
|
106
|
+
);"""
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return output_path
|
|
110
|
+
|
|
111
|
+
def _render_flask(self, renderer: Any, output_path: Path) -> Path:
|
|
112
|
+
"""Render pages as a Flask project."""
|
|
113
|
+
templates_dir = output_path / "templates" / "pages"
|
|
114
|
+
static_dir = output_path / "static"
|
|
115
|
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
route_names = []
|
|
119
|
+
for i, page in enumerate(self.pages):
|
|
120
|
+
page_title = page.title or f"Page{i}"
|
|
121
|
+
route_name = renderer._to_snake_case(page_title)
|
|
122
|
+
route_names.append(route_name)
|
|
123
|
+
|
|
124
|
+
content = renderer.render_page(page, title=self.title)
|
|
125
|
+
(templates_dir / f"{route_name}.html").write_text(content)
|
|
126
|
+
|
|
127
|
+
# Generate base.html
|
|
128
|
+
(output_path / "templates" / "base.html").write_text(
|
|
129
|
+
renderer.generate_base_html(self.title)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Generate app.py
|
|
133
|
+
(output_path / "app.py").write_text(
|
|
134
|
+
renderer.generate_app_py(self.title, route_names)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Generate requirements.txt
|
|
138
|
+
(output_path / "requirements.txt").write_text(
|
|
139
|
+
renderer.generate_requirements()
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return output_path
|
|
143
|
+
|
|
144
|
+
def _render_django(self, renderer: Any, output_path: Path) -> Path:
|
|
145
|
+
"""Render pages as a Django project."""
|
|
146
|
+
project_name = renderer._to_snake_case(self.title)
|
|
147
|
+
templates_dir = output_path / "templates" / "pages"
|
|
148
|
+
app_dir = output_path / project_name
|
|
149
|
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
|
|
152
|
+
route_names = []
|
|
153
|
+
for i, page in enumerate(self.pages):
|
|
154
|
+
page_title = page.title or f"Page{i}"
|
|
155
|
+
route_name = renderer._to_snake_case(page_title)
|
|
156
|
+
route_names.append(route_name)
|
|
157
|
+
|
|
158
|
+
content = renderer.render_page(page, title=self.title)
|
|
159
|
+
(templates_dir / f"{route_name}.html").write_text(content)
|
|
160
|
+
|
|
161
|
+
# Generate base.html
|
|
162
|
+
(output_path / "templates" / "base.html").write_text(
|
|
163
|
+
renderer.generate_base_html(self.title)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Generate views.py
|
|
167
|
+
(app_dir / "views.py").write_text(
|
|
168
|
+
renderer.generate_views_py(self.title, route_names)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Generate urls.py
|
|
172
|
+
(app_dir / "urls.py").write_text(
|
|
173
|
+
renderer.generate_urls_py(route_names)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Generate settings.py
|
|
177
|
+
(output_path / "settings.py").write_text(
|
|
178
|
+
renderer.generate_settings_py(self.title)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Generate requirements.txt
|
|
182
|
+
(output_path / "requirements.txt").write_text(
|
|
183
|
+
renderer.generate_requirements()
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return output_path
|
|
187
|
+
|
|
188
|
+
def _get_renderer(self, name: str) -> Any:
|
|
189
|
+
"""Import and instantiate a renderer by name."""
|
|
190
|
+
if name == "lnative":
|
|
191
|
+
from uigen.renderers.lnative import LNativeRenderer
|
|
192
|
+
return LNativeRenderer(theme=self.theme)
|
|
193
|
+
elif name == "lreact":
|
|
194
|
+
from uigen.renderers.lreact import LReactRenderer
|
|
195
|
+
return LReactRenderer()
|
|
196
|
+
elif name == "lflask":
|
|
197
|
+
from uigen.renderers.lflask import LFlaskRenderer
|
|
198
|
+
return LFlaskRenderer()
|
|
199
|
+
elif name == "ldjango":
|
|
200
|
+
from uigen.renderers.ldjango import LDjangoRenderer
|
|
201
|
+
return LDjangoRenderer()
|
|
202
|
+
else:
|
|
203
|
+
available = "lnative, lreact, lflask, ldjango"
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Unknown renderer: {name}. Available: {available}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def __repr__(self) -> str:
|
|
209
|
+
return (
|
|
210
|
+
f"App(title={self.title!r}, "
|
|
211
|
+
f"pages={len(self.pages)}, models={len(self.models)})"
|
|
212
|
+
)
|