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 +27 -0
- htmlforge/__main__.py +5 -0
- htmlforge/cli.py +194 -0
- htmlforge/core.py +687 -0
- htmlforge/simple.py +331 -0
- htmlforge-0.0.1.dist-info/METADATA +205 -0
- htmlforge-0.0.1.dist-info/RECORD +10 -0
- htmlforge-0.0.1.dist-info/WHEEL +4 -0
- htmlforge-0.0.1.dist-info/entry_points.txt +2 -0
- htmlforge-0.0.1.dist-info/licenses/LICENSE +1 -0
htmlforge/simple.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""htmlforge simple — High-level API with built-in themes.
|
|
2
|
+
|
|
3
|
+
Designed for Python programmers who don't know HTML.
|
|
4
|
+
Everything looks great out of the box — no CSS required.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from htmlforge import Doc
|
|
9
|
+
|
|
10
|
+
page = Doc("My Site")
|
|
11
|
+
page.heading("Hello World")
|
|
12
|
+
page.text("Some **bold** and *italic* text")
|
|
13
|
+
page.button("Click me", on_click="alert('hi')")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
from typing import Union
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from htmlforge.core import (
|
|
21
|
+
Page, Element, Div, Hr, Text,
|
|
22
|
+
H1, H2, H3, P, Strong, Em, Link,
|
|
23
|
+
Ul, Ol, Li, Img,
|
|
24
|
+
Table, Thead, Tbody, Tr, Th, Td,
|
|
25
|
+
Input, Textarea, Select, Option, Button as _Button,
|
|
26
|
+
Details, Summary, Code, Pre, _to_kebab,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================
|
|
31
|
+
# Default Theme — clean, modern, responsive
|
|
32
|
+
# ============================================================
|
|
33
|
+
|
|
34
|
+
_DEFAULT_CSS = """
|
|
35
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
36
|
+
body {
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
38
|
+
'Helvetica Neue', Arial, sans-serif;
|
|
39
|
+
line-height: 1.7; max-width: 52rem;
|
|
40
|
+
margin: 0 auto; padding: 2rem 1.5rem;
|
|
41
|
+
color: #1d1d1f; background: #fff;
|
|
42
|
+
}
|
|
43
|
+
h1 { font-size: 2.2rem; font-weight: 700; margin: 2rem 0 .8rem; }
|
|
44
|
+
h2 { font-size: 1.6rem; font-weight: 600; margin: 1.8rem 0 .6rem; }
|
|
45
|
+
h3 { font-size: 1.25rem; font-weight: 600; margin: 1.4rem 0 .5rem; }
|
|
46
|
+
p { margin: 0 0 1rem; }
|
|
47
|
+
a { color: #0066cc; text-decoration: none; }
|
|
48
|
+
a:hover { text-decoration: underline; }
|
|
49
|
+
img { max-width: 100%; height: auto; border-radius: 8px; }
|
|
50
|
+
/* ---- button ---- */
|
|
51
|
+
.py-btn {
|
|
52
|
+
display: inline-block; padding: .55rem 1.4rem;
|
|
53
|
+
font-size: .95rem; font-weight: 500;
|
|
54
|
+
border: 2px solid #0066cc; border-radius: 8px;
|
|
55
|
+
background: #0066cc; color: #fff;
|
|
56
|
+
cursor: pointer; transition: all .2s;
|
|
57
|
+
margin: .25rem .25rem .25rem 0;
|
|
58
|
+
}
|
|
59
|
+
.py-btn:hover { background: #0052a3; border-color: #0052a3; }
|
|
60
|
+
/* ---- table ---- */
|
|
61
|
+
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
|
62
|
+
th, td { padding: .6rem .8rem; text-align: left;
|
|
63
|
+
border-bottom: 1px solid #e5e7eb; }
|
|
64
|
+
th { background: #f8f9fa; font-weight: 600; }
|
|
65
|
+
tr:hover td { background: #f5f7fa; }
|
|
66
|
+
/* ---- card ---- */
|
|
67
|
+
.py-card {
|
|
68
|
+
border: 1px solid #e5e7eb; border-radius: 12px;
|
|
69
|
+
padding: 1.4rem; margin: 1rem 0;
|
|
70
|
+
transition: box-shadow .2s;
|
|
71
|
+
}
|
|
72
|
+
.py-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.06); }
|
|
73
|
+
.py-card h3:first-child,
|
|
74
|
+
.py-card h2:first-child { margin-top: 0; }
|
|
75
|
+
/* ---- grid ---- */
|
|
76
|
+
.py-grid { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
77
|
+
.py-grid > * { flex: 1 1 16rem; min-width: 0; }
|
|
78
|
+
/* ---- input / textarea ---- */
|
|
79
|
+
.py-input, .py-textarea {
|
|
80
|
+
display: block; width: 100%;
|
|
81
|
+
padding: .5rem .75rem; font-size: .95rem;
|
|
82
|
+
border: 1px solid #d1d5db; border-radius: 8px;
|
|
83
|
+
margin: .4rem 0; outline: none;
|
|
84
|
+
transition: border-color .2s;
|
|
85
|
+
}
|
|
86
|
+
.py-input:focus, .py-textarea:focus { border-color: #0066cc; }
|
|
87
|
+
/* ---- select ---- */
|
|
88
|
+
.py-select {
|
|
89
|
+
display: block; width: 100%;
|
|
90
|
+
padding: .5rem .75rem; font-size: .95rem;
|
|
91
|
+
border: 1px solid #d1d5db; border-radius: 8px;
|
|
92
|
+
margin: .4rem 0;
|
|
93
|
+
}
|
|
94
|
+
/* ---- code ---- */
|
|
95
|
+
pre { background: #f4f4f5; padding: 1rem; border-radius: 8px;
|
|
96
|
+
overflow-x: auto; font-size: .9rem; }
|
|
97
|
+
code { font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace; }
|
|
98
|
+
p code { background: #f4f4f5; padding: .15rem .3rem; border-radius: 4px; }
|
|
99
|
+
/* ---- list ---- */
|
|
100
|
+
ul, ol { padding-left: 1.5rem; margin: 0 0 1rem; }
|
|
101
|
+
li { margin: .2rem 0; }
|
|
102
|
+
/* ---- details ---- */
|
|
103
|
+
details { margin: .5rem 0; }
|
|
104
|
+
details > summary {
|
|
105
|
+
cursor: pointer; font-weight: 500;
|
|
106
|
+
padding: .5rem 0; user-select: none;
|
|
107
|
+
}
|
|
108
|
+
details > summary:hover { color: #0066cc; }
|
|
109
|
+
/* ---- nav ---- */
|
|
110
|
+
.py-nav {
|
|
111
|
+
display: flex; gap: 1.5rem; padding: .5rem 0;
|
|
112
|
+
margin-bottom: 1rem; border-bottom: 1px solid #e5e7eb;
|
|
113
|
+
}
|
|
114
|
+
/* ---- hr ---- */
|
|
115
|
+
hr { border: none; border-top: 1px solid #e5e7eb; margin: 2rem 0; }
|
|
116
|
+
""".strip()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ============================================================
|
|
120
|
+
# Markdown inline helpers
|
|
121
|
+
# ============================================================
|
|
122
|
+
|
|
123
|
+
def _md(s: str) -> str:
|
|
124
|
+
"""Convert inline Markdown to HTML."""
|
|
125
|
+
s = re.sub(r'`(.+?)`', r'<code>\1</code>', s)
|
|
126
|
+
s = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', s)
|
|
127
|
+
s = re.sub(r'\*(.+?)\*', r'<em>\1</em>', s)
|
|
128
|
+
s = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', s)
|
|
129
|
+
return s
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ============================================================
|
|
133
|
+
# Doc — the high-level page builder
|
|
134
|
+
# ============================================================
|
|
135
|
+
|
|
136
|
+
class Doc:
|
|
137
|
+
"""Build a styled web page using only Python — no HTML needed.
|
|
138
|
+
|
|
139
|
+
Comparison::
|
|
140
|
+
|
|
141
|
+
# Low-level (core.py) # High-level (simple.py)
|
|
142
|
+
page = Page("Site") page = Doc("Site")
|
|
143
|
+
page.add_css("body{...}") # theme built-in
|
|
144
|
+
page.add(H1("Hi")) page.heading("Hi")
|
|
145
|
+
page.add(P("text")) page.text("text")
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, title: str = "Untitled", *,
|
|
149
|
+
lang: str = "zh", theme: bool = True):
|
|
150
|
+
self._page = Page(title, lang=lang)
|
|
151
|
+
if theme:
|
|
152
|
+
self._page.add_css(_DEFAULT_CSS)
|
|
153
|
+
|
|
154
|
+
# ---- delegate to Page ----
|
|
155
|
+
|
|
156
|
+
def add_css(self, css: str) -> Doc:
|
|
157
|
+
"""Add extra CSS rules."""
|
|
158
|
+
self._page.add_css(css)
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def add_script(self, code: str = "", src: str = "") -> Doc:
|
|
162
|
+
"""Add JavaScript (inline code or external src)."""
|
|
163
|
+
self._page.add_script(code, src)
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def add_raw(self, html: str) -> Doc:
|
|
167
|
+
"""Insert raw HTML string."""
|
|
168
|
+
self._page.add_raw(html)
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def add(self, *children: Union[Element, str]) -> Doc:
|
|
172
|
+
"""Add raw Elements or text (auto-wraps strings in <p>)."""
|
|
173
|
+
for c in children:
|
|
174
|
+
if isinstance(c, str):
|
|
175
|
+
self._page.add(P(Text(_md(c))))
|
|
176
|
+
else:
|
|
177
|
+
self._page.add(c)
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
# ---- text & headings ----
|
|
181
|
+
|
|
182
|
+
def heading(self, text: str, level: int = 1) -> Doc:
|
|
183
|
+
"""Add a heading (level 1-6)."""
|
|
184
|
+
tags = {1: H1, 2: H2, 3: H3}
|
|
185
|
+
return self.add(tags.get(level, H3)(text))
|
|
186
|
+
|
|
187
|
+
def text(self, content: str) -> Doc:
|
|
188
|
+
"""Add a paragraph. Supports **bold**, *italic*, `code`, [link](url)."""
|
|
189
|
+
self._page.add(P(Text(_md(content))))
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def link(self, text: str, url: str) -> Doc:
|
|
193
|
+
"""Add a hyperlink."""
|
|
194
|
+
return self.add(Link(text, url))
|
|
195
|
+
|
|
196
|
+
def image(self, src: str, alt: str = "") -> Doc:
|
|
197
|
+
"""Add an image (auto-responsive)."""
|
|
198
|
+
return self.add(Img(src, alt))
|
|
199
|
+
|
|
200
|
+
# ---- layout ----
|
|
201
|
+
|
|
202
|
+
def card(self, title: str = "", *content: Union[Element, str]) -> Doc:
|
|
203
|
+
"""Add a card with optional title and content."""
|
|
204
|
+
return self.add(self.make_card(title, *content))
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def make_card(title: str = "", *content: Union[Element, str]) -> Div:
|
|
208
|
+
"""Create a card element (use with ``row`` / ``grid``)."""
|
|
209
|
+
d = Div(class_="py-card")
|
|
210
|
+
if title:
|
|
211
|
+
d.add(H3(title))
|
|
212
|
+
for c in content:
|
|
213
|
+
if isinstance(c, str):
|
|
214
|
+
d.add(P(Text(_md(c))))
|
|
215
|
+
else:
|
|
216
|
+
d.add(c)
|
|
217
|
+
return d
|
|
218
|
+
|
|
219
|
+
def row(self, *children: Element) -> Doc:
|
|
220
|
+
"""Arrange elements side-by-side in a flex row."""
|
|
221
|
+
return self.add(Div(class_="py-grid").add(*children))
|
|
222
|
+
|
|
223
|
+
def grid(self, *children: Element, cols: int = 0) -> Doc:
|
|
224
|
+
"""Responsive grid layout. ``cols=0`` means auto-fit."""
|
|
225
|
+
d = Div(class_="py-grid")
|
|
226
|
+
if cols:
|
|
227
|
+
d.css(gridTemplateColumns=f"repeat({cols}, 1fr)")
|
|
228
|
+
d.add(*children)
|
|
229
|
+
return self.add(d)
|
|
230
|
+
|
|
231
|
+
# ---- interactive ----
|
|
232
|
+
|
|
233
|
+
def button(self, label: str, on_click: str = "", *,
|
|
234
|
+
color: str = "") -> Doc:
|
|
235
|
+
"""Add a button. Set ``color`` for custom background."""
|
|
236
|
+
btn = _Button(class_="py-btn")
|
|
237
|
+
btn.add(label)
|
|
238
|
+
if color:
|
|
239
|
+
btn.css(background=color, borderColor=color)
|
|
240
|
+
if on_click:
|
|
241
|
+
btn.attr("onclick", on_click)
|
|
242
|
+
return self.add(btn)
|
|
243
|
+
|
|
244
|
+
def input(self, placeholder: str = "", *,
|
|
245
|
+
type: str = "text", name: str = "") -> Doc:
|
|
246
|
+
"""Add a text input field."""
|
|
247
|
+
return self.add(Input(type_=type, name=name,
|
|
248
|
+
placeholder=placeholder, class_="py-input"))
|
|
249
|
+
|
|
250
|
+
def textarea(self, placeholder: str = "", *,
|
|
251
|
+
rows: str = "4", name: str = "") -> Doc:
|
|
252
|
+
"""Add a multi-line text area."""
|
|
253
|
+
return self.add(Textarea(placeholder, name=name,
|
|
254
|
+
rows=rows, class_="py-textarea"))
|
|
255
|
+
|
|
256
|
+
def select(self, options: list[str], *,
|
|
257
|
+
name: str = "") -> Doc:
|
|
258
|
+
"""Add a dropdown. ``options`` is a list of label strings."""
|
|
259
|
+
pairs = [(o, o) for o in options]
|
|
260
|
+
return self.add(Select(options=pairs, name=name, class_="py-select"))
|
|
261
|
+
|
|
262
|
+
# ---- data ----
|
|
263
|
+
|
|
264
|
+
def list(self, items: list[str], *, ordered: bool = False) -> Doc:
|
|
265
|
+
"""Add a bullet list (or numbered if ``ordered=True``)."""
|
|
266
|
+
cls: type = Ol if ordered else Ul
|
|
267
|
+
el = cls()
|
|
268
|
+
for item in items:
|
|
269
|
+
el.add(Li(item))
|
|
270
|
+
return self.add(el)
|
|
271
|
+
|
|
272
|
+
def table(self, headers: list[str],
|
|
273
|
+
rows: list[list[str]]) -> Doc:
|
|
274
|
+
"""Add a table."""
|
|
275
|
+
return self.add(Table(headers, rows))
|
|
276
|
+
|
|
277
|
+
def code(self, source: str, *, lang: str = "") -> Doc:
|
|
278
|
+
"""Add a code block."""
|
|
279
|
+
c = Code(source)
|
|
280
|
+
if lang:
|
|
281
|
+
c.attr("class", f"language-{lang}")
|
|
282
|
+
return self.add(Pre().add(c))
|
|
283
|
+
|
|
284
|
+
# ---- extras ----
|
|
285
|
+
|
|
286
|
+
def details(self, summary: str, *content: Union[Element, str]) -> Doc:
|
|
287
|
+
"""Add a collapsible section."""
|
|
288
|
+
det = Details().add(Summary(summary))
|
|
289
|
+
for c in content:
|
|
290
|
+
if isinstance(c, str):
|
|
291
|
+
det.add(P(Text(_md(c))))
|
|
292
|
+
else:
|
|
293
|
+
det.add(c)
|
|
294
|
+
return self.add(det)
|
|
295
|
+
|
|
296
|
+
def nav(self, *links: tuple[str, str]) -> Doc:
|
|
297
|
+
"""Add a navigation bar. Each arg is ``(text, url)``."""
|
|
298
|
+
n = Div(class_="py-nav")
|
|
299
|
+
for label, url in links:
|
|
300
|
+
n.add(Link(label, url))
|
|
301
|
+
return self.add(n)
|
|
302
|
+
|
|
303
|
+
def divider(self) -> Doc:
|
|
304
|
+
"""Add a horizontal rule."""
|
|
305
|
+
return self.add(Hr())
|
|
306
|
+
|
|
307
|
+
# ---- style (programmatic CSS) ----
|
|
308
|
+
|
|
309
|
+
def style(self, selector: str, **props: str) -> Doc:
|
|
310
|
+
"""Add a CSS rule using Python kwargs.
|
|
311
|
+
|
|
312
|
+
::
|
|
313
|
+
|
|
314
|
+
page.style("body", background="#f0f0f0", font_family="sans-serif")
|
|
315
|
+
page.style("h1", color="navy", font_size="2rem")
|
|
316
|
+
page.style(".card", border_radius="12px", padding="1rem")
|
|
317
|
+
page.style("button:hover", background="darkblue")
|
|
318
|
+
"""
|
|
319
|
+
decls = "; ".join(
|
|
320
|
+
f"{_to_kebab(k)}: {v}" for k, v in props.items()
|
|
321
|
+
)
|
|
322
|
+
self._page.add_css(f"{selector} {{ {decls} }}")
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
# ---- rendering ----
|
|
326
|
+
|
|
327
|
+
def render(self) -> str:
|
|
328
|
+
return self._page.render()
|
|
329
|
+
|
|
330
|
+
def __str__(self) -> str:
|
|
331
|
+
return self.render()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: htmlforge
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Build web pages with Python, export to HTML with a single command
|
|
5
|
+
Project-URL: Homepage, https://github.com/htmlforge/htmlforge
|
|
6
|
+
Author: htmlforge contributors
|
|
7
|
+
License-Expression: MPL-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: builder,html,page,static-site,web
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# htmlforge
|
|
20
|
+
|
|
21
|
+
Build web pages with Python, export to HTML with a single command — like manim generates videos.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
manim scene.py MyScene → mp4
|
|
25
|
+
htmlforge page.py MyPage → html
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install htmlforge
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Development install:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Write a Python page script `mysite.py`
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from htmlforge import Doc
|
|
46
|
+
|
|
47
|
+
page = Doc("My Site")
|
|
48
|
+
|
|
49
|
+
# Generate styles with Python code (no CSS needed!)
|
|
50
|
+
page.style("body", background="#f5f5f5", font_family="sans-serif")
|
|
51
|
+
page.style(".py-card", border_radius="12px", box_shadow="0 2px 8px rgba(0,0,0,.1)")
|
|
52
|
+
|
|
53
|
+
# Build the page
|
|
54
|
+
page.heading("Welcome to My Site")
|
|
55
|
+
page.text("Built with **htmlforge**, supports *Markdown* syntax")
|
|
56
|
+
|
|
57
|
+
page.card("Features",
|
|
58
|
+
"40+ HTML components",
|
|
59
|
+
"Generate styles with Python code",
|
|
60
|
+
"Export HTML with one command",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
page.table(
|
|
64
|
+
["Feature", "Description"],
|
|
65
|
+
[
|
|
66
|
+
["Components", "40+ HTML components"],
|
|
67
|
+
["Built-in theme", "Works out of the box"],
|
|
68
|
+
["CLI tools", "render / serve / watch"],
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
page.button("Click me", on_click="alert('Hello!')", color="#4CAF50")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Run the command to export HTML
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Basic render
|
|
79
|
+
htmlforge render mysite.py
|
|
80
|
+
|
|
81
|
+
# Specify output directory
|
|
82
|
+
htmlforge render mysite.py -o build
|
|
83
|
+
|
|
84
|
+
# Render and start a local server
|
|
85
|
+
htmlforge render mysite.py -s
|
|
86
|
+
|
|
87
|
+
# Watch for file changes and auto-rebuild
|
|
88
|
+
htmlforge render mysite.py -w
|
|
89
|
+
|
|
90
|
+
# Render a specific page (when file has multiple pages)
|
|
91
|
+
htmlforge render mysite.py MyPage
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## CLI Commands
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `htmlforge render <file.py>` | Render all pages to `dist/` |
|
|
99
|
+
| `htmlforge render <file.py> -o <dir>` | Specify output directory |
|
|
100
|
+
| `htmlforge render <file.py> <page>` | Render only the specified page |
|
|
101
|
+
| `htmlforge render <file.py> -s` | Start HTTP server after render |
|
|
102
|
+
| `htmlforge render <file.py> -s -p 3000` | Specify server port |
|
|
103
|
+
| `htmlforge render <file.py> -w` | Watch for changes and auto-rebuild |
|
|
104
|
+
|
|
105
|
+
## Doc API — Ultra Simple
|
|
106
|
+
|
|
107
|
+
`Doc` is a high-level API designed for Python programmers who don't know HTML. It comes with a built-in theme — no CSS required to build great-looking pages.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from htmlforge import Doc
|
|
111
|
+
|
|
112
|
+
page = Doc("Title")
|
|
113
|
+
|
|
114
|
+
# Generate styles with Python kwargs (no CSS strings!)
|
|
115
|
+
page.style("body", background="linear-gradient(135deg, #667eea, #764ba2)", color="white")
|
|
116
|
+
page.style("h1", font_size="3rem", text_align="center")
|
|
117
|
+
page.style(".py-card", background="rgba(255,255,255,.15)", backdrop_filter="blur(10px)")
|
|
118
|
+
|
|
119
|
+
# Content (supports **Markdown** syntax)
|
|
120
|
+
page.heading("Hello World")
|
|
121
|
+
page.text("Supports **bold**, *italic*, `code`, [links](url)")
|
|
122
|
+
|
|
123
|
+
page.card("Card Title", "Card content, **supports Markdown**")
|
|
124
|
+
page.list(["Item A", "Item B", "Item C"])
|
|
125
|
+
page.table(["Col 1", "Col 2"], [["a", "b"], ["c", "d"]])
|
|
126
|
+
page.button("Button", on_click="alert('hi')", color="#e74c3c")
|
|
127
|
+
page.input("Type here...", name="name")
|
|
128
|
+
page.select(["Option A", "Option B", "Option C"])
|
|
129
|
+
page.code("print('hello')", lang="python")
|
|
130
|
+
page.details("Collapsible", "Hidden content...")
|
|
131
|
+
page.nav(("Home", "/"), ("About", "/about"))
|
|
132
|
+
page.divider()
|
|
133
|
+
|
|
134
|
+
# Layout
|
|
135
|
+
page.row(Doc.make_card("A"), Doc.make_card("B"), Doc.make_card("C"))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Page API — Fine-grained Control
|
|
139
|
+
|
|
140
|
+
Use `Page` + the component system when you need more control:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from htmlforge import Page, H1, P, Div, Table, Button
|
|
144
|
+
|
|
145
|
+
page = Page("Title", lang="en")
|
|
146
|
+
page.add_css("body { font-family: sans-serif; }")
|
|
147
|
+
page.add(
|
|
148
|
+
H1("Welcome"),
|
|
149
|
+
P("Hello World"),
|
|
150
|
+
Div(class_="card").add(
|
|
151
|
+
H1("Features", level=2),
|
|
152
|
+
P("Content..."),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Element — Chainable API
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
div = Div(id="main", class_="wrapper")
|
|
161
|
+
div.add(H1("Title"), P("Content")) # Add child elements
|
|
162
|
+
div.css(color="red", font_size="16px") # Set inline styles (camelCase → kebab-case)
|
|
163
|
+
div.attr("data-id", "123") # Set attributes
|
|
164
|
+
html = div.render() # → HTML string
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Components
|
|
168
|
+
|
|
169
|
+
| Category | Components |
|
|
170
|
+
|---|---|
|
|
171
|
+
| **Structure** | `Div`, `Section`, `Header`, `Footer`, `Nav`, `Main`, `Article`, `Aside`, `Span`, `Container` |
|
|
172
|
+
| **Text** | `H1`-`H6`, `P`, `Strong`, `Em`, `Blockquote`, `Code`, `Pre`, `Small`, `Mark` |
|
|
173
|
+
| **Links/Media** | `Link`, `Img`, `Video`, `Audio`, `Iframe`, `Canvas` |
|
|
174
|
+
| **Lists** | `Ul`, `Ol`, `Li`, `List` |
|
|
175
|
+
| **Tables** | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td`, `Caption` |
|
|
176
|
+
| **Forms** | `Form`, `Input`, `Textarea`, `Select`, `Option`, `Checkbox`, `Radio`, `Label`, `Button` |
|
|
177
|
+
| **Other** | `Hr`, `Br`, `Text`, `Progress`, `Details`, `Summary` |
|
|
178
|
+
|
|
179
|
+
## Example: Multi-page Site
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# site.py
|
|
183
|
+
from htmlforge import Doc
|
|
184
|
+
|
|
185
|
+
home = Doc("Home")
|
|
186
|
+
home.heading("Home")
|
|
187
|
+
home.text("Welcome to the home page")
|
|
188
|
+
|
|
189
|
+
about = Doc("About")
|
|
190
|
+
about.heading("About Us")
|
|
191
|
+
about.text("This is the about page")
|
|
192
|
+
|
|
193
|
+
blog = Doc("Blog")
|
|
194
|
+
blog.heading("Blog")
|
|
195
|
+
blog.text("Latest posts...")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
htmlforge render site.py
|
|
200
|
+
# → dist/home.html, dist/about.html, dist/blog.html
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[MPL-2.0](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
htmlforge/__init__.py,sha256=om4GflxTfsgUBh4DWnbUhpASW5sXrlddrqPkzYZWFN0,725
|
|
2
|
+
htmlforge/__main__.py,sha256=I2IO0PQiXvzFeSkTADZYK3wjwCZocQqqZz-MGESrg14,104
|
|
3
|
+
htmlforge/cli.py,sha256=lrGhEaRqLaFeQybORduNSJOTmvrm2V777-GAbjoFZKI,6171
|
|
4
|
+
htmlforge/core.py,sha256=5i0Mxi3w0Pp6SvR8OYixP5PGF8x51DN8UCxFALc3Gl8,18731
|
|
5
|
+
htmlforge/simple.py,sha256=S3iXOWtneiUWJZUBaLqlrxtyTj84OpIMdQlVkyNvOt4,11487
|
|
6
|
+
htmlforge-0.0.1.dist-info/METADATA,sha256=Gy8a4RZEtmsQb85QT0mnMLmxJ5TS3NXwLTgPJRbP64A,5749
|
|
7
|
+
htmlforge-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
htmlforge-0.0.1.dist-info/entry_points.txt,sha256=4NFQHme4dPb8WjKirGaMRPa2jBE6X8WpZwZTwi7gqUQ,51
|
|
9
|
+
htmlforge-0.0.1.dist-info/licenses/LICENSE,sha256=H_SFL1HRjJcNiNCqq5FQPxzPKE9FL_A5qfYcPz9rdqg,36
|
|
10
|
+
htmlforge-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Mozilla Public License Version 2.0
|