htmlforge 0.0.1__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.
htmlforge/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """htmlforge — Build web pages with Python, export to HTML."""
2
+
3
+ __version__ = "0.0.1"
4
+
5
+ from htmlforge.simple import Doc
6
+
7
+ from htmlforge.core import (
8
+ # Base
9
+ Element, Page,
10
+ # Structural
11
+ Div, Section, Header, Footer, Nav, Main, Article, Aside, Span, Container,
12
+ # Text
13
+ H1, H2, H3, H4, H5, H6, P, Strong, Em, Blockquote,
14
+ Code, Pre, Small, Mark, Sub, Sup,
15
+ Text, Link,
16
+ # Lists
17
+ Ul, Ol, Li, List,
18
+ # Media
19
+ Img, Video, Audio, Source, Iframe, Canvas,
20
+ # Table
21
+ Table, Thead, Tbody, Tr, Th, Td, Caption,
22
+ # Forms
23
+ Form, Input, Textarea, Select, Option,
24
+ Checkbox, Radio, Label, Button,
25
+ # Misc
26
+ Hr, Br, Meta, Progress, Details, Summary,
27
+ )
htmlforge/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow ``python -m htmlforge`` to invoke the CLI."""
2
+
3
+ from htmlforge.cli import render
4
+
5
+ render()
htmlforge/cli.py ADDED
@@ -0,0 +1,194 @@
1
+ """htmlforge CLI — build HTML from Python page scripts.
2
+
3
+ Commands:
4
+ htmlforge render <file.py> [page] -o <dir> Render pages to HTML
5
+ htmlforge render <file.py> -p <port> Render and serve locally
6
+ htmlforge render <file.py> -w Watch and auto-rebuild
7
+
8
+ Workflow is modelled after *manim*:
9
+ manim scene.py MyScene -qh → mp4
10
+ htmlforge page.py MyPage -o dist → html
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import http.server
17
+ import importlib.util
18
+ import os
19
+ import re
20
+ import socketserver
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ from htmlforge.core import Page
26
+ from htmlforge.simple import Doc
27
+
28
+
29
+ # ============================================================
30
+ # Entry point
31
+ # ============================================================
32
+
33
+ def render():
34
+ """``[project.scripts]`` entry — installed as the ``htmlforge`` command."""
35
+ ap = argparse.ArgumentParser(
36
+ prog="htmlforge",
37
+ description="htmlforge — Build web pages with Python",
38
+ )
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")
53
+
54
+ args = ap.parse_args()
55
+
56
+ if args.command == "render":
57
+ _do_render(args)
58
+ else:
59
+ ap.print_help()
60
+
61
+
62
+ # ============================================================
63
+ # Render
64
+ # ============================================================
65
+
66
+ def _do_render(args):
67
+ filepath = Path(args.file).resolve()
68
+ if not filepath.exists():
69
+ _err(f"File not found: {filepath}")
70
+ sys.exit(1)
71
+
72
+ # Initial build
73
+ ok = _build(filepath, args.page, args.output, args.verbose)
74
+ if not ok:
75
+ sys.exit(1)
76
+
77
+ if args.serve:
78
+ out_dir = filepath.parent / args.output
79
+ _serve_dir(str(out_dir), args.port)
80
+ elif args.watch:
81
+ _watch_loop(filepath, args.page, args.output, args.verbose)
82
+
83
+
84
+ def _build(filepath: Path, page_name: str | None,
85
+ output: str, verbose: bool) -> bool:
86
+ try:
87
+ mod = _load_module(filepath)
88
+ except Exception as exc:
89
+ _err(f"Error loading {filepath.name}: {exc}")
90
+ return False
91
+
92
+ pages = _discover_pages(mod)
93
+
94
+ if page_name:
95
+ key = page_name.lower()
96
+ pages = {k: v for k, v in pages.items() if k == key}
97
+ if not pages:
98
+ _err(
99
+ f"Page '{page_name}' not found. "
100
+ f"Available: {', '.join(_discover_pages(mod))}"
101
+ )
102
+ return False
103
+
104
+ if not pages:
105
+ _err("No Page objects found in the file.")
106
+ return False
107
+
108
+ out_dir = filepath.parent / output
109
+ out_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ for name, page in pages.items():
112
+ html = page.render()
113
+ fname = "index.html" if len(pages) == 1 else f"{_slug(name)}.html"
114
+ (out_dir / fname).write_text(html, encoding="utf-8")
115
+ _ok(f" {fname}")
116
+
117
+ _info(f"Done. {len(pages)} page(s) → {out_dir}/")
118
+ return True
119
+
120
+
121
+ # ============================================================
122
+ # Serve
123
+ # ============================================================
124
+
125
+ def _serve_dir(directory: str, port: int):
126
+ os.chdir(directory)
127
+ Handler = http.server.SimpleHTTPRequestHandler
128
+ with socketserver.TCPServer(("", port), Handler) as httpd:
129
+ _info(f" Serving at http://localhost:{port}")
130
+ _info(" Press Ctrl+C to stop.")
131
+ try:
132
+ httpd.serve_forever()
133
+ except KeyboardInterrupt:
134
+ _info("\n Server stopped.")
135
+
136
+
137
+ # ============================================================
138
+ # Watch
139
+ # ============================================================
140
+
141
+ def _watch_loop(filepath: Path, page_name: str | None,
142
+ output: str, verbose: bool):
143
+ _info(" Watching for changes… (Ctrl+C to stop)")
144
+ last = filepath.stat().st_mtime
145
+ try:
146
+ while True:
147
+ time.sleep(1)
148
+ try:
149
+ cur = filepath.stat().st_mtime
150
+ except FileNotFoundError:
151
+ continue
152
+ if cur != last:
153
+ last = cur
154
+ _info(f" Change detected — {time.strftime('%H:%M:%S')}")
155
+ _build(filepath, page_name, output, verbose)
156
+ except KeyboardInterrupt:
157
+ _info("\n Watch stopped.")
158
+
159
+
160
+ # ============================================================
161
+ # Helpers
162
+ # ============================================================
163
+
164
+ def _ok(msg: str):
165
+ print(f"\033[32m{msg}\033[0m")
166
+
167
+ def _info(msg: str):
168
+ print(f"\033[36m{msg}\033[0m")
169
+
170
+ def _err(msg: str):
171
+ print(f"\033[31mError: {msg}\033[0m", file=sys.stderr)
172
+
173
+
174
+ def _load_module(filepath: Path):
175
+ """Dynamically import a Python file as a module."""
176
+ spec = importlib.util.spec_from_file_location("_htmlforge_user", str(filepath))
177
+ if spec is None or spec.loader is None:
178
+ raise ImportError(f"Cannot load {filepath}")
179
+ mod = importlib.util.module_from_spec(spec)
180
+ spec.loader.exec_module(mod)
181
+ return mod
182
+
183
+
184
+ def _discover_pages(mod) -> dict:
185
+ """Find all ``Page`` and ``Doc`` instances at module level."""
186
+ return {
187
+ name: obj
188
+ for name, obj in vars(mod).items()
189
+ if isinstance(obj, (Page, Doc)) and not name.startswith("_")
190
+ }
191
+
192
+
193
+ def _slug(name: str) -> str:
194
+ return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")