···11+# minihtml - Simple HTML Generation
22+33+
44+55+The minihtml library provides tools to generate HTML from Python. The goal of
66+minihtml is to enable you to write HTML mixed with presentation logic without
77+obscuring the structure of the resulting HTML document, not unlike the
88+experience with a text-based templating system such as [Jinja][jinja].
99+1010+jinja: https://jinja.palletsprojects.com/
1111+1212+## Installation
1313+1414+Install the `minihtml` package from PyPI:
1515+1616+ pip install minihtml
1717+1818+or
1919+2020+ uv add minihtml
2121+2222+## Example
2323+2424+A basic "hello, world" example:
2525+2626+~~~python
2727+>>> from minihtml.tags import html, head, title, body, div, p, a, img
2828+>>> with html(lang="en") as elem:
2929+... with head:
3030+... title("hello, world!")
3131+... with body:
3232+... with div["#content main"]:
3333+... p("Welcome to ", a(href="https://example.com/")("my website"))
3434+... img(src="hello.png", alt="hello")
3535+...
3636+<ElementNonEmpty title>
3737+<ElementNonEmpty p>
3838+<ElementEmpty img>
3939+4040+>>> print(elem)
4141+<html lang="en">
4242+ <head>
4343+ <title>hello, world!</title>
4444+ </head>
4545+ <body>
4646+ <div id="content" class="main">
4747+ <p>Welcome to <a href="https://example.com/">my website</a></p>
4848+ <img src="hello.png" alt="hello">
4949+ </div>
5050+ </body>
5151+</html>
5252+5353+~~~
5454+5555+## Prototypes and Elements
5656+5757+The objects you import from `minihtml.tags` are called *prototypes*. You can
5858+think of them as HTML element factories. Prototypes are immutable. There are
5959+several ways you can interact with a prototype to produce an actual *element*:
6060+6161+Calling the prototype produces an element:
6262+6363+~~~python
6464+>>> print(div)
6565+<PrototypeNonEmpty div>
6666+>>> elem = div()
6767+>>> repr(elem)
6868+'<ElementNonEmpty div>'
6969+>>> print(elem)
7070+<div></div>
7171+7272+~~~
7373+7474+Positional arguments get converted to children of the new element, and keyword
7575+arguments to attributes:
7676+7777+~~~python
7878+>>> print(div("text"))
7979+<div>text</div>
8080+>>> print(div(style="background: green"))
8181+<div style="background: green"></div>
8282+8383+~~~
8484+8585+## Element Content and Attributes
8686+8787+Content can be text, other elements or a mix of the two:
8888+8989+~~~python
9090+>>> from minihtml.tags import em
9191+>>> print(div("this is ", em("emphasized text"), "."))
9292+<div>this is <em>emphasized text</em>.</div>
9393+9494+~~~
9595+9696+You can chain calls and all other operations on the element to modify it
9797+further. Each operation modifies the element in-place (elements are mutable).
9898+9999+~~~python
100100+>>> print(a(href="http://github.com")("github"))
101101+<a href="http://github.com">github</a>
102102+103103+~~~
104104+105105+Indexing is a shortcut to set the `class` and `id` attributes:
106106+107107+~~~python
108108+>>> print(div["hello"])
109109+<div class="hello"></div>
110110+>>> print(div["#main"])
111111+<div id="main"></div>
112112+>>> print(div["#main foo bar"])
113113+<div id="main" class="foo bar"></div>
114114+>>> print(div["foo"]["bar"])
115115+<div class="foo bar"></div>
116116+117117+~~~
118118+119119+Finally, you can build up a document declaratively by using elements or
120120+prototypes as context managers. New elements created within the context will be
121121+added as children to the parent element:
122122+123123+~~~python
124124+>>> from minihtml.tags import ul, li
125125+>>> with div["main"] as elem:
126126+... with ul:
127127+... for color in ("red", "green", "blue"):
128128+... li(color)
129129+...
130130+<ElementNonEmpty li>
131131+<ElementNonEmpty li>
132132+<ElementNonEmpty li>
133133+134134+>>> print(elem)
135135+<div class="main">
136136+ <ul>
137137+ <li>red</li>
138138+ <li>green</li>
139139+ <li>blue</li>
140140+ </ul>
141141+</div>
142142+143143+~~~
144144+145145+Elements that are passed to another element as a positional argument are exempt
146146+from this, so you can mix the two styles:
147147+148148+~~~python
149149+>>> with div as elem:
150150+... p(em("this is emphasized"))
151151+...
152152+<ElementNonEmpty p>
153153+154154+# The `em` element does *not* also end up as a child of `div`.
155155+>>> print(elem)
156156+<div>
157157+ <p><em>this is emphasized</em></p>
158158+</div>
159159+160160+~~~
161161+162162+<!-- TODO document attribute name mangling -->
163163+<!-- TODO document components -->
164164+<!-- TODO document @template -->
+5-1
justfile
···11# Generate, format, typecheck and test code
22-all: codegen format typecheck test
22+all: codegen format typecheck test doctest
3344# Run code generation
55codegen:
···1919 uv run coverage run -m pytest
2020 uv run coverage report
2121 uv run coverage html
2222+2323+# Run doctests
2424+doctest:
2525+ uv run python -m doctest -o ELLIPSIS README.md
22262327# Run tests when code changes (requires "watchexec")
2428watch: