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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: htmlforge
3
- Version: 0.0.1
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 scene.py MyScene → mp4
25
- htmlforge page.py MyPage → html
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 code (no CSS needed!)
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 scene.py MyScene → mp4
7
- htmlforge page.py MyPage → html
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 code (no CSS needed!)
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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "htmlforge"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "Build web pages with Python, export to HTML with a single command"
9
9
  readme = "README.md"
10
10
  license = "MPL-2.0"
@@ -1,8 +1,9 @@
1
1
  """htmlforge — Build web pages with Python, export to HTML."""
2
2
 
3
- __version__ = "0.0.1"
3
+ __version__ = "0.0.2"
4
4
 
5
5
  from htmlforge.simple import Doc
6
+ from htmlforge.transpiler import py_to_js, convert_file, JS_HELPERS
6
7
 
7
8
  from htmlforge.core import (
8
9
  # Base
@@ -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.add_argument(
40
- "command", nargs="?", default="render",
41
- help="Command: render (default)",
42
- )
43
- ap.add_argument("file", help="Python file containing Page objects")
44
- ap.add_argument(
45
- "page", nargs="?", default=None,
46
- help="Specific page variable or class name to render",
47
- )
48
- ap.add_argument("-o", "--output", default="dist", help="Output directory (default: dist)")
49
- ap.add_argument("-p", "--port", type=int, default=8080, help="Server port (default: 8080)")
50
- ap.add_argument("-w", "--watch", action="store_true", help="Watch file for changes")
51
- ap.add_argument("-s", "--serve", action="store_true", help="Start HTTP server after build")
52
- ap.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
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
- ap.print_help()
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: