htmlforge 0.0.1__tar.gz → 0.0.2__tar.gz
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.
- {htmlforge-0.0.1 → htmlforge-0.0.2}/PKG-INFO +129 -11
- {htmlforge-0.0.1 → htmlforge-0.0.2}/README.md +128 -10
- {htmlforge-0.0.1 → htmlforge-0.0.2}/pyproject.toml +1 -1
- {htmlforge-0.0.1 → htmlforge-0.0.2}/src/htmlforge/__init__.py +2 -1
- {htmlforge-0.0.1 → htmlforge-0.0.2}/src/htmlforge/cli.py +64 -15
- {htmlforge-0.0.1 → htmlforge-0.0.2}/src/htmlforge/core.py +27 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/src/htmlforge/simple.py +120 -0
- htmlforge-0.0.2/src/htmlforge/transpiler.py +639 -0
- htmlforge-0.0.2/tests/test_transpiler.py +446 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/.gitignore +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/LICENSE +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/demo.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/demo_simple.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/src/htmlforge/__main__.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/style_demo.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/tests/__init__.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/tests/buttons.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/tests/test_cli.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/tests/test_core.py +0 -0
- {htmlforge-0.0.1 → htmlforge-0.0.2}/tests/test_simple.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: htmlforge
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Build web pages with Python, export to HTML with a single command
|
|
5
5
|
Project-URL: Homepage, https://github.com/htmlforge/htmlforge
|
|
6
6
|
Author: htmlforge contributors
|
|
@@ -21,8 +21,9 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
Build web pages with Python, export to HTML with a single command — like manim generates videos.
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
manim
|
|
25
|
-
htmlforge
|
|
24
|
+
manim scene.py MyScene → mp4
|
|
25
|
+
htmlforge page.py MyPage → html
|
|
26
|
+
htmlforge script app.py → js
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
## Installation
|
|
@@ -31,12 +32,6 @@ htmlforge page.py MyPage → html
|
|
|
31
32
|
pip install htmlforge
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Development install:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pip install -e ".[dev]"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
35
|
## Quick Start
|
|
41
36
|
|
|
42
37
|
### 1. Write a Python page script `mysite.py`
|
|
@@ -46,7 +41,7 @@ from htmlforge import Doc
|
|
|
46
41
|
|
|
47
42
|
page = Doc("My Site")
|
|
48
43
|
|
|
49
|
-
# Generate styles with Python
|
|
44
|
+
# Generate styles with Python kwargs (no CSS strings!)
|
|
50
45
|
page.style("body", background="#f5f5f5", font_family="sans-serif")
|
|
51
46
|
page.style(".py-card", border_radius="12px", box_shadow="0 2px 8px rgba(0,0,0,.1)")
|
|
52
47
|
|
|
@@ -65,7 +60,7 @@ page.table(
|
|
|
65
60
|
[
|
|
66
61
|
["Components", "40+ HTML components"],
|
|
67
62
|
["Built-in theme", "Works out of the box"],
|
|
68
|
-
["CLI tools", "render / serve / watch"],
|
|
63
|
+
["CLI tools", "render / serve / watch / script"],
|
|
69
64
|
],
|
|
70
65
|
)
|
|
71
66
|
|
|
@@ -101,6 +96,9 @@ htmlforge render mysite.py MyPage
|
|
|
101
96
|
| `htmlforge render <file.py> -s` | Start HTTP server after render |
|
|
102
97
|
| `htmlforge render <file.py> -s -p 3000` | Specify server port |
|
|
103
98
|
| `htmlforge render <file.py> -w` | Watch for changes and auto-rebuild |
|
|
99
|
+
| `htmlforge script <file.py>` | Convert Python file to JavaScript |
|
|
100
|
+
| `htmlforge script <file.py> -o app.js` | Specify output JS filename |
|
|
101
|
+
| `htmlforge script <file.py> --helpers` | Include DOM helper functions |
|
|
104
102
|
|
|
105
103
|
## Doc API — Ultra Simple
|
|
106
104
|
|
|
@@ -135,6 +133,124 @@ page.divider()
|
|
|
135
133
|
page.row(Doc.make_card("A"), Doc.make_card("B"), Doc.make_card("C"))
|
|
136
134
|
```
|
|
137
135
|
|
|
136
|
+
## JavaScript & DOM Interaction
|
|
137
|
+
|
|
138
|
+
### Bind events to elements
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from htmlforge import Button
|
|
142
|
+
|
|
143
|
+
btn = Button("Click me")
|
|
144
|
+
|
|
145
|
+
# Bind raw JavaScript
|
|
146
|
+
btn.on("click", "alert('hello')")
|
|
147
|
+
btn.on("mouseover", "this.style.color='red'")
|
|
148
|
+
|
|
149
|
+
# Bind Python code (auto-converted to JS)
|
|
150
|
+
btn.on_py("click", "print('clicked!')")
|
|
151
|
+
# → onclick="console.log('clicked!');"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Add JavaScript to the page
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Raw JavaScript
|
|
158
|
+
page.js("document.title = 'Dynamic'")
|
|
159
|
+
page.js("function greet() { alert('hello'); }")
|
|
160
|
+
|
|
161
|
+
# External JS file
|
|
162
|
+
page.js_file("app.js")
|
|
163
|
+
|
|
164
|
+
# Python code → JavaScript (auto-converted)
|
|
165
|
+
page.py_script('''
|
|
166
|
+
def greet(name):
|
|
167
|
+
print(f"Hello {name}")
|
|
168
|
+
greet("World")
|
|
169
|
+
''')
|
|
170
|
+
|
|
171
|
+
# Run on page load (DOMContentLoaded)
|
|
172
|
+
page.on_ready("console.log('Page loaded!')")
|
|
173
|
+
page.on_ready_py('print("Ready!")')
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### DOM manipulation helpers
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# Get element properties (returns JS expressions)
|
|
180
|
+
val = page.get_attr("#input", "value") # → document.querySelector('#input').getAttribute('value')
|
|
181
|
+
text = page.get_text("#title") # → document.querySelector('#title').textContent
|
|
182
|
+
html = page.get_html("#content") # → document.querySelector('#content').innerHTML
|
|
183
|
+
value = page.get_value("#myInput") # → document.querySelector('#myInput').value
|
|
184
|
+
|
|
185
|
+
# Set element properties (generates JS scripts)
|
|
186
|
+
page.set_attr("#el", "class", "active")
|
|
187
|
+
page.set_text("#title", "New Title")
|
|
188
|
+
page.set_html("#content", "<b>bold</b>")
|
|
189
|
+
page.set_value("#input", "hello")
|
|
190
|
+
|
|
191
|
+
# Visibility
|
|
192
|
+
page.show("#modal")
|
|
193
|
+
page.hide("#loading")
|
|
194
|
+
page.toggle("#sidebar")
|
|
195
|
+
|
|
196
|
+
# CSS class manipulation
|
|
197
|
+
page.add_class("#el", "active")
|
|
198
|
+
page.remove_class("#el", "hidden")
|
|
199
|
+
page.toggle_class("#menu", "open")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Python → JavaScript Transpiler
|
|
203
|
+
|
|
204
|
+
Use `htmlforge script` to convert Python files to JavaScript:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
htmlforge script app.py # → app.js
|
|
208
|
+
htmlforge script app.py -o out.js # → out.js
|
|
209
|
+
htmlforge script app.py --helpers # include DOM helpers ($, $$, getAttr, setAttr, ...)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or use it programmatically:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from htmlforge import py_to_js, convert_file
|
|
216
|
+
|
|
217
|
+
# Convert Python source string to JS
|
|
218
|
+
js = py_to_js("""
|
|
219
|
+
def greet(name):
|
|
220
|
+
print(f"Hello {name}")
|
|
221
|
+
|
|
222
|
+
for i in range(10):
|
|
223
|
+
greet("World")
|
|
224
|
+
""")
|
|
225
|
+
|
|
226
|
+
# Convert a file
|
|
227
|
+
convert_file("app.py", "app.js")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Supported Python → JS conversions
|
|
231
|
+
|
|
232
|
+
| Python | JavaScript |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `print(...)` | `console.log(...)` |
|
|
235
|
+
| `input(...)` | `prompt(...)` |
|
|
236
|
+
| `def f(x):` | `function f(x) {` |
|
|
237
|
+
| `class Foo(Bar):` | `class Foo extends Bar {` |
|
|
238
|
+
| `self.x` | `this.x` |
|
|
239
|
+
| `if/elif/else:` | `if/else if/else {` |
|
|
240
|
+
| `for i in range(n):` | `for (let i = 0; i < n; i++) {` |
|
|
241
|
+
| `for x in items:` | `for (let x of items) {` |
|
|
242
|
+
| `while cond:` | `while (cond) {` |
|
|
243
|
+
| `try/except/finally:` | `try/catch/finally {` |
|
|
244
|
+
| `True / False / None` | `true / false / null` |
|
|
245
|
+
| `and / or / not` | `&& / \|\| / !` |
|
|
246
|
+
| `a // b` | `Math.floor(a / b)` |
|
|
247
|
+
| `a ** b` | `Math.pow(a, b)` |
|
|
248
|
+
| `len(x)` | `x.length` |
|
|
249
|
+
| `f"Hello {name}"` | `` `Hello ${name}` `` |
|
|
250
|
+
| `lambda x: x + 1` | `(x) => x + 1` |
|
|
251
|
+
| `x in items` | `items.includes(x)` |
|
|
252
|
+
| `sorted(items)` | `[...items].sort()` |
|
|
253
|
+
|
|
138
254
|
## Page API — Fine-grained Control
|
|
139
255
|
|
|
140
256
|
Use `Page` + the component system when you need more control:
|
|
@@ -161,6 +277,8 @@ div = Div(id="main", class_="wrapper")
|
|
|
161
277
|
div.add(H1("Title"), P("Content")) # Add child elements
|
|
162
278
|
div.css(color="red", font_size="16px") # Set inline styles (camelCase → kebab-case)
|
|
163
279
|
div.attr("data-id", "123") # Set attributes
|
|
280
|
+
div.on("click", "alert('clicked')") # Bind JS event
|
|
281
|
+
div.on_py("click", "print('clicked')") # Bind Python event (auto-converted)
|
|
164
282
|
html = div.render() # → HTML string
|
|
165
283
|
```
|
|
166
284
|
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Build web pages with Python, export to HTML with a single command — like manim generates videos.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
manim
|
|
7
|
-
htmlforge
|
|
6
|
+
manim scene.py MyScene → mp4
|
|
7
|
+
htmlforge page.py MyPage → html
|
|
8
|
+
htmlforge script app.py → js
|
|
8
9
|
```
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
@@ -13,12 +14,6 @@ htmlforge page.py MyPage → html
|
|
|
13
14
|
pip install htmlforge
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
Development install:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pip install -e ".[dev]"
|
|
20
|
-
```
|
|
21
|
-
|
|
22
17
|
## Quick Start
|
|
23
18
|
|
|
24
19
|
### 1. Write a Python page script `mysite.py`
|
|
@@ -28,7 +23,7 @@ from htmlforge import Doc
|
|
|
28
23
|
|
|
29
24
|
page = Doc("My Site")
|
|
30
25
|
|
|
31
|
-
# Generate styles with Python
|
|
26
|
+
# Generate styles with Python kwargs (no CSS strings!)
|
|
32
27
|
page.style("body", background="#f5f5f5", font_family="sans-serif")
|
|
33
28
|
page.style(".py-card", border_radius="12px", box_shadow="0 2px 8px rgba(0,0,0,.1)")
|
|
34
29
|
|
|
@@ -47,7 +42,7 @@ page.table(
|
|
|
47
42
|
[
|
|
48
43
|
["Components", "40+ HTML components"],
|
|
49
44
|
["Built-in theme", "Works out of the box"],
|
|
50
|
-
["CLI tools", "render / serve / watch"],
|
|
45
|
+
["CLI tools", "render / serve / watch / script"],
|
|
51
46
|
],
|
|
52
47
|
)
|
|
53
48
|
|
|
@@ -83,6 +78,9 @@ htmlforge render mysite.py MyPage
|
|
|
83
78
|
| `htmlforge render <file.py> -s` | Start HTTP server after render |
|
|
84
79
|
| `htmlforge render <file.py> -s -p 3000` | Specify server port |
|
|
85
80
|
| `htmlforge render <file.py> -w` | Watch for changes and auto-rebuild |
|
|
81
|
+
| `htmlforge script <file.py>` | Convert Python file to JavaScript |
|
|
82
|
+
| `htmlforge script <file.py> -o app.js` | Specify output JS filename |
|
|
83
|
+
| `htmlforge script <file.py> --helpers` | Include DOM helper functions |
|
|
86
84
|
|
|
87
85
|
## Doc API — Ultra Simple
|
|
88
86
|
|
|
@@ -117,6 +115,124 @@ page.divider()
|
|
|
117
115
|
page.row(Doc.make_card("A"), Doc.make_card("B"), Doc.make_card("C"))
|
|
118
116
|
```
|
|
119
117
|
|
|
118
|
+
## JavaScript & DOM Interaction
|
|
119
|
+
|
|
120
|
+
### Bind events to elements
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from htmlforge import Button
|
|
124
|
+
|
|
125
|
+
btn = Button("Click me")
|
|
126
|
+
|
|
127
|
+
# Bind raw JavaScript
|
|
128
|
+
btn.on("click", "alert('hello')")
|
|
129
|
+
btn.on("mouseover", "this.style.color='red'")
|
|
130
|
+
|
|
131
|
+
# Bind Python code (auto-converted to JS)
|
|
132
|
+
btn.on_py("click", "print('clicked!')")
|
|
133
|
+
# → onclick="console.log('clicked!');"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Add JavaScript to the page
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Raw JavaScript
|
|
140
|
+
page.js("document.title = 'Dynamic'")
|
|
141
|
+
page.js("function greet() { alert('hello'); }")
|
|
142
|
+
|
|
143
|
+
# External JS file
|
|
144
|
+
page.js_file("app.js")
|
|
145
|
+
|
|
146
|
+
# Python code → JavaScript (auto-converted)
|
|
147
|
+
page.py_script('''
|
|
148
|
+
def greet(name):
|
|
149
|
+
print(f"Hello {name}")
|
|
150
|
+
greet("World")
|
|
151
|
+
''')
|
|
152
|
+
|
|
153
|
+
# Run on page load (DOMContentLoaded)
|
|
154
|
+
page.on_ready("console.log('Page loaded!')")
|
|
155
|
+
page.on_ready_py('print("Ready!")')
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### DOM manipulation helpers
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# Get element properties (returns JS expressions)
|
|
162
|
+
val = page.get_attr("#input", "value") # → document.querySelector('#input').getAttribute('value')
|
|
163
|
+
text = page.get_text("#title") # → document.querySelector('#title').textContent
|
|
164
|
+
html = page.get_html("#content") # → document.querySelector('#content').innerHTML
|
|
165
|
+
value = page.get_value("#myInput") # → document.querySelector('#myInput').value
|
|
166
|
+
|
|
167
|
+
# Set element properties (generates JS scripts)
|
|
168
|
+
page.set_attr("#el", "class", "active")
|
|
169
|
+
page.set_text("#title", "New Title")
|
|
170
|
+
page.set_html("#content", "<b>bold</b>")
|
|
171
|
+
page.set_value("#input", "hello")
|
|
172
|
+
|
|
173
|
+
# Visibility
|
|
174
|
+
page.show("#modal")
|
|
175
|
+
page.hide("#loading")
|
|
176
|
+
page.toggle("#sidebar")
|
|
177
|
+
|
|
178
|
+
# CSS class manipulation
|
|
179
|
+
page.add_class("#el", "active")
|
|
180
|
+
page.remove_class("#el", "hidden")
|
|
181
|
+
page.toggle_class("#menu", "open")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Python → JavaScript Transpiler
|
|
185
|
+
|
|
186
|
+
Use `htmlforge script` to convert Python files to JavaScript:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
htmlforge script app.py # → app.js
|
|
190
|
+
htmlforge script app.py -o out.js # → out.js
|
|
191
|
+
htmlforge script app.py --helpers # include DOM helpers ($, $$, getAttr, setAttr, ...)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or use it programmatically:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from htmlforge import py_to_js, convert_file
|
|
198
|
+
|
|
199
|
+
# Convert Python source string to JS
|
|
200
|
+
js = py_to_js("""
|
|
201
|
+
def greet(name):
|
|
202
|
+
print(f"Hello {name}")
|
|
203
|
+
|
|
204
|
+
for i in range(10):
|
|
205
|
+
greet("World")
|
|
206
|
+
""")
|
|
207
|
+
|
|
208
|
+
# Convert a file
|
|
209
|
+
convert_file("app.py", "app.js")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Supported Python → JS conversions
|
|
213
|
+
|
|
214
|
+
| Python | JavaScript |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `print(...)` | `console.log(...)` |
|
|
217
|
+
| `input(...)` | `prompt(...)` |
|
|
218
|
+
| `def f(x):` | `function f(x) {` |
|
|
219
|
+
| `class Foo(Bar):` | `class Foo extends Bar {` |
|
|
220
|
+
| `self.x` | `this.x` |
|
|
221
|
+
| `if/elif/else:` | `if/else if/else {` |
|
|
222
|
+
| `for i in range(n):` | `for (let i = 0; i < n; i++) {` |
|
|
223
|
+
| `for x in items:` | `for (let x of items) {` |
|
|
224
|
+
| `while cond:` | `while (cond) {` |
|
|
225
|
+
| `try/except/finally:` | `try/catch/finally {` |
|
|
226
|
+
| `True / False / None` | `true / false / null` |
|
|
227
|
+
| `and / or / not` | `&& / \|\| / !` |
|
|
228
|
+
| `a // b` | `Math.floor(a / b)` |
|
|
229
|
+
| `a ** b` | `Math.pow(a, b)` |
|
|
230
|
+
| `len(x)` | `x.length` |
|
|
231
|
+
| `f"Hello {name}"` | `` `Hello ${name}` `` |
|
|
232
|
+
| `lambda x: x + 1` | `(x) => x + 1` |
|
|
233
|
+
| `x in items` | `items.includes(x)` |
|
|
234
|
+
| `sorted(items)` | `[...items].sort()` |
|
|
235
|
+
|
|
120
236
|
## Page API — Fine-grained Control
|
|
121
237
|
|
|
122
238
|
Use `Page` + the component system when you need more control:
|
|
@@ -143,6 +259,8 @@ div = Div(id="main", class_="wrapper")
|
|
|
143
259
|
div.add(H1("Title"), P("Content")) # Add child elements
|
|
144
260
|
div.css(color="red", font_size="16px") # Set inline styles (camelCase → kebab-case)
|
|
145
261
|
div.attr("data-id", "123") # Set attributes
|
|
262
|
+
div.on("click", "alert('clicked')") # Bind JS event
|
|
263
|
+
div.on_py("click", "print('clicked')") # Bind Python event (auto-converted)
|
|
146
264
|
html = div.render() # → HTML string
|
|
147
265
|
```
|
|
148
266
|
|
|
@@ -4,10 +4,12 @@ Commands:
|
|
|
4
4
|
htmlforge render <file.py> [page] -o <dir> Render pages to HTML
|
|
5
5
|
htmlforge render <file.py> -p <port> Render and serve locally
|
|
6
6
|
htmlforge render <file.py> -w Watch and auto-rebuild
|
|
7
|
+
htmlforge script <file.py> [-o <file.js>] Convert Python to JavaScript
|
|
7
8
|
|
|
8
9
|
Workflow is modelled after *manim*:
|
|
9
10
|
manim scene.py MyScene -qh → mp4
|
|
10
11
|
htmlforge page.py MyPage -o dist → html
|
|
12
|
+
htmlforge script app.py -o app.js → js
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -24,6 +26,7 @@ from pathlib import Path
|
|
|
24
26
|
|
|
25
27
|
from htmlforge.core import Page
|
|
26
28
|
from htmlforge.simple import Doc
|
|
29
|
+
from htmlforge.transpiler import convert_file as _convert_py_to_js
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
# ============================================================
|
|
@@ -36,27 +39,45 @@ def render():
|
|
|
36
39
|
prog="htmlforge",
|
|
37
40
|
description="htmlforge — Build web pages with Python",
|
|
38
41
|
)
|
|
39
|
-
ap.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
sub = ap.add_subparsers(dest="command", help="Command to run")
|
|
43
|
+
sub.required = False
|
|
44
|
+
|
|
45
|
+
# ---- render ----
|
|
46
|
+
render_p = sub.add_parser("render", help="Render pages to HTML")
|
|
47
|
+
render_p.add_argument("file", help="Python file containing Page objects")
|
|
48
|
+
render_p.add_argument("page", nargs="?", default=None,
|
|
49
|
+
help="Specific page variable or class name to render")
|
|
50
|
+
render_p.add_argument("-o", "--output", default="dist",
|
|
51
|
+
help="Output directory (default: dist)")
|
|
52
|
+
render_p.add_argument("-p", "--port", type=int, default=8080,
|
|
53
|
+
help="Server port (default: 8080)")
|
|
54
|
+
render_p.add_argument("-w", "--watch", action="store_true",
|
|
55
|
+
help="Watch file for changes")
|
|
56
|
+
render_p.add_argument("-s", "--serve", action="store_true",
|
|
57
|
+
help="Start HTTP server after build")
|
|
58
|
+
render_p.add_argument("-v", "--verbose", action="store_true",
|
|
59
|
+
help="Verbose output")
|
|
60
|
+
|
|
61
|
+
# ---- script ----
|
|
62
|
+
script_p = sub.add_parser("script", help="Convert Python file to JavaScript")
|
|
63
|
+
script_p.add_argument("file", help="Python file to convert")
|
|
64
|
+
script_p.add_argument("-o", "--output", default=None,
|
|
65
|
+
help="Output JS file (default: same name .js)")
|
|
66
|
+
script_p.add_argument("--helpers", action="store_true",
|
|
67
|
+
help="Include DOM helper functions")
|
|
53
68
|
|
|
54
69
|
args = ap.parse_args()
|
|
55
70
|
|
|
56
71
|
if args.command == "render":
|
|
57
72
|
_do_render(args)
|
|
73
|
+
elif args.command == "script":
|
|
74
|
+
_do_script(args)
|
|
58
75
|
else:
|
|
59
|
-
|
|
76
|
+
# Backward compat: first positional arg is a file
|
|
77
|
+
if hasattr(args, 'file') and args.file:
|
|
78
|
+
_do_render(args)
|
|
79
|
+
else:
|
|
80
|
+
ap.print_help()
|
|
60
81
|
|
|
61
82
|
|
|
62
83
|
# ============================================================
|
|
@@ -157,6 +178,34 @@ def _watch_loop(filepath: Path, page_name: str | None,
|
|
|
157
178
|
_info("\n Watch stopped.")
|
|
158
179
|
|
|
159
180
|
|
|
181
|
+
# ============================================================
|
|
182
|
+
# Script (Python → JS)
|
|
183
|
+
# ============================================================
|
|
184
|
+
|
|
185
|
+
def _do_script(args):
|
|
186
|
+
filepath = Path(args.file).resolve()
|
|
187
|
+
if not filepath.exists():
|
|
188
|
+
_err(f"File not found: {filepath}")
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
output = args.output
|
|
192
|
+
if output is None:
|
|
193
|
+
output = str(filepath.with_suffix(".js"))
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
from htmlforge.transpiler import py_to_js, JS_HELPERS
|
|
197
|
+
source = filepath.read_text(encoding="utf-8")
|
|
198
|
+
js_code = py_to_js(source)
|
|
199
|
+
if args.helpers:
|
|
200
|
+
js_code = JS_HELPERS + "\n\n" + js_code
|
|
201
|
+
Path(output).write_text(js_code, encoding="utf-8")
|
|
202
|
+
_ok(f" {filepath.name} → {Path(output).name}")
|
|
203
|
+
_info(f"Done. JavaScript written to {output}")
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
_err(f"Conversion failed: {exc}")
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
160
209
|
# ============================================================
|
|
161
210
|
# Helpers
|
|
162
211
|
# ============================================================
|
|
@@ -54,6 +54,33 @@ class Element:
|
|
|
54
54
|
self.attrs.pop(key, None)
|
|
55
55
|
return self
|
|
56
56
|
|
|
57
|
+
def on(self, event: str, handler: str) -> Element:
|
|
58
|
+
"""Bind an event handler (raw JS code).
|
|
59
|
+
|
|
60
|
+
::
|
|
61
|
+
|
|
62
|
+
btn.on("click", "alert('hello')")
|
|
63
|
+
btn.on("mouseover", "this.style.color='red'")
|
|
64
|
+
"""
|
|
65
|
+
attr_name = f"on{event}"
|
|
66
|
+
existing = self.attrs.get(attr_name, "")
|
|
67
|
+
if existing:
|
|
68
|
+
self.attrs[attr_name] = f"{existing}; {handler}"
|
|
69
|
+
else:
|
|
70
|
+
self.attrs[attr_name] = handler
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def on_py(self, event: str, py_code: str) -> Element:
|
|
74
|
+
"""Bind an event handler written in Python (converted to JS).
|
|
75
|
+
|
|
76
|
+
::
|
|
77
|
+
|
|
78
|
+
btn.on_py("click", "print('clicked')")
|
|
79
|
+
"""
|
|
80
|
+
from htmlforge.transpiler import py_to_js
|
|
81
|
+
js_code = py_to_js(py_code)
|
|
82
|
+
return self.on(event, js_code)
|
|
83
|
+
|
|
57
84
|
# --- rendering ---
|
|
58
85
|
|
|
59
86
|
def _open_tag(self) -> str:
|
|
@@ -304,6 +304,126 @@ class Doc:
|
|
|
304
304
|
"""Add a horizontal rule."""
|
|
305
305
|
return self.add(Hr())
|
|
306
306
|
|
|
307
|
+
# ---- JavaScript & DOM helpers ----
|
|
308
|
+
|
|
309
|
+
def js(self, code: str) -> Doc:
|
|
310
|
+
"""Add raw JavaScript code to the page.
|
|
311
|
+
|
|
312
|
+
::
|
|
313
|
+
|
|
314
|
+
page.js("document.title = 'Dynamic'")
|
|
315
|
+
page.js("function greet() { alert('hello'); }")
|
|
316
|
+
"""
|
|
317
|
+
self._page.add_script(code=code)
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
def js_file(self, src: str) -> Doc:
|
|
321
|
+
"""Link an external JavaScript file."""
|
|
322
|
+
self._page.add_script(src=src)
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
def py_script(self, code: str) -> Doc:
|
|
326
|
+
"""Add Python code that gets converted to JavaScript.
|
|
327
|
+
|
|
328
|
+
::
|
|
329
|
+
|
|
330
|
+
page.py_script('def greet(name): print(name)')
|
|
331
|
+
"""
|
|
332
|
+
from htmlforge.transpiler import py_to_js
|
|
333
|
+
js_code = py_to_js(code)
|
|
334
|
+
self._page.add_script(code=js_code)
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
def on_ready(self, js_code: str) -> Doc:
|
|
338
|
+
"""Run JS code when the page loads (DOMContentLoaded)."""
|
|
339
|
+
wrapped = f"document.addEventListener('DOMContentLoaded', function() {{\n{js_code}\n}});"
|
|
340
|
+
self._page.add_script(code=wrapped)
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def on_ready_py(self, py_code: str) -> Doc:
|
|
344
|
+
"""Run Python code (converted to JS) when the page loads."""
|
|
345
|
+
from htmlforge.transpiler import py_to_js
|
|
346
|
+
js_code = py_to_js(py_code)
|
|
347
|
+
return self.on_ready(js_code)
|
|
348
|
+
|
|
349
|
+
# ---- DOM attribute helpers (generate JS) ----
|
|
350
|
+
|
|
351
|
+
def get_attr(self, selector: str, attr: str) -> str:
|
|
352
|
+
"""Return JS expression to get an element's attribute.
|
|
353
|
+
|
|
354
|
+
::
|
|
355
|
+
|
|
356
|
+
page.js(f"let val = {page.get_attr('#myInput', 'value')}")
|
|
357
|
+
"""
|
|
358
|
+
return f"document.querySelector('{selector}').getAttribute('{attr}')"
|
|
359
|
+
|
|
360
|
+
def set_attr(self, selector: str, attr: str, value: str) -> Doc:
|
|
361
|
+
"""Add JS code to set an element's attribute."""
|
|
362
|
+
self.js(f"document.querySelector('{selector}').setAttribute('{attr}', '{value}');")
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def get_text(self, selector: str) -> str:
|
|
366
|
+
"""Return JS expression to get element's text content."""
|
|
367
|
+
return f"document.querySelector('{selector}').textContent"
|
|
368
|
+
|
|
369
|
+
def set_text(self, selector: str, text: str) -> Doc:
|
|
370
|
+
"""Add JS code to set element's text content."""
|
|
371
|
+
self.js(f"document.querySelector('{selector}').textContent = '{text}';")
|
|
372
|
+
return self
|
|
373
|
+
|
|
374
|
+
def get_html(self, selector: str) -> str:
|
|
375
|
+
"""Return JS expression to get element's innerHTML."""
|
|
376
|
+
return f"document.querySelector('{selector}').innerHTML"
|
|
377
|
+
|
|
378
|
+
def set_html(self, selector: str, html: str) -> Doc:
|
|
379
|
+
"""Add JS code to set element's innerHTML."""
|
|
380
|
+
self.js(f"document.querySelector('{selector}').innerHTML = `{html}`;")
|
|
381
|
+
return self
|
|
382
|
+
|
|
383
|
+
def get_value(self, selector: str) -> str:
|
|
384
|
+
"""Return JS expression to get an input's value."""
|
|
385
|
+
return f"document.querySelector('{selector}').value"
|
|
386
|
+
|
|
387
|
+
def set_value(self, selector: str, value: str) -> Doc:
|
|
388
|
+
"""Add JS code to set an input's value."""
|
|
389
|
+
self.js(f"document.querySelector('{selector}').value = '{value}';")
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def show(self, selector: str) -> Doc:
|
|
393
|
+
"""Add JS code to show an element."""
|
|
394
|
+
self.js(f"document.querySelector('{selector}').style.display = '';")
|
|
395
|
+
return self
|
|
396
|
+
|
|
397
|
+
def hide(self, selector: str) -> Doc:
|
|
398
|
+
"""Add JS code to hide an element."""
|
|
399
|
+
self.js(f"document.querySelector('{selector}').style.display = 'none';")
|
|
400
|
+
return self
|
|
401
|
+
|
|
402
|
+
def toggle(self, selector: str) -> Doc:
|
|
403
|
+
"""Add JS code to toggle an element's visibility."""
|
|
404
|
+
self.js(f"""
|
|
405
|
+
(function() {{
|
|
406
|
+
var el = document.querySelector('{selector}');
|
|
407
|
+
el.style.display = el.style.display === 'none' ? '' : 'none';
|
|
408
|
+
}})();
|
|
409
|
+
""")
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
def add_class(self, selector: str, cls: str) -> Doc:
|
|
413
|
+
"""Add JS code to add a CSS class."""
|
|
414
|
+
self.js(f"document.querySelector('{selector}').classList.add('{cls}');")
|
|
415
|
+
return self
|
|
416
|
+
|
|
417
|
+
def remove_class(self, selector: str, cls: str) -> Doc:
|
|
418
|
+
"""Add JS code to remove a CSS class."""
|
|
419
|
+
self.js(f"document.querySelector('{selector}').classList.remove('{cls}');")
|
|
420
|
+
return self
|
|
421
|
+
|
|
422
|
+
def toggle_class(self, selector: str, cls: str) -> Doc:
|
|
423
|
+
"""Add JS code to toggle a CSS class."""
|
|
424
|
+
self.js(f"document.querySelector('{selector}').classList.toggle('{cls}');")
|
|
425
|
+
return self
|
|
426
|
+
|
|
307
427
|
# ---- style (programmatic CSS) ----
|
|
308
428
|
|
|
309
429
|
def style(self, selector: str, **props: str) -> Doc:
|