Simple HTML Generation https://minihtml.trendels.name/
1.. currentmodule:: minihtml
2
3.. TODO The outline can be improved.
4
5.. _basics:
6
7Basics
8======
9
10Installation
11------------
12
13Install the `minihtml` package from PyPI:
14
15.. code-block:: console
16
17 $ pip install minihtml
18
19or:
20
21.. code-block:: console
22
23 $ uv add minihtml
24
25.. _prototypes:
26
27Prototypes
28----------
29
30All minihtml HTML elements are created from a *Prototype*. A prototype is
31an immutable element factory that produces :ref:`elements`.
32
33You can import the prototypes for all HTML5 elements from :ref:`minihtml.tags <tags>`:
34
35>>> from minihtml.tags import html, head, title
36>>> html
37<PrototypeNonEmpty html>
38
39Converting an element to a string returns its HTML representation. There are
40several ways to produce an element from a prototype.
41
42Calling the prototype produces a new element:
43
44>>> elem = html()
45>>> elem
46<ElementNonEmpty html>
47>>> print(elem)
48<html></html>
49
50Positional arguments add content to the element. Content can be other elements
51or strings:
52
53>>> elem = html(head(title("hello, world!")))
54>>> print(elem)
55<html>
56 <head>
57 <title>hello, world!</title>
58 </head>
59</html>
60
61
62Keyword arguments are converted to HTML :ref:`attributes <attributes>`:
63
64>>> elem = html(lang="en")
65>>> print(elem)
66<html lang="en"></html>
67
68Indexing a prototype with a string is a convenient way to set the new element's
69``class`` and/or ``id`` attributes:
70
71>>> from minihtml.tags import div
72>>> elem = div["#my-id class-a class-b"]
73>>> print(elem)
74<div id="my-id" class="class-a class-b"></div>
75
76Using a prototype as a context manager creates an *element context*: New
77elements created within the context are added as children to the parent element
78(the element returned by the context manager):
79
80>>> with html as elem: # creates an element context with parent element `elem` (html)
81... with head: # creates a nested element context
82... title("hello, world!")
83<...>
84>>> print(elem)
85<html>
86 <head>
87 <title>hello, world!</title>
88 </head>
89</html>
90
91As you can see in the example above, it does not matter how the elements are
92created, from a context manager (``with head:``) or by calling (``title()``),
93they are always added to the parent element of the containing context. There is
94one important exception: Elements passed as positional arguments to another
95element or prototype are exempt from this, so this works as expected:
96
97>>> from minihtml.tags import p, em
98>>> with div as elem:
99... p(em("this em element is a child of p, not div"))
100<...>
101>>> print(elem)
102<div>
103 <p><em>this em element is a child of p, not div</em></p>
104</div>
105
106Finally, you can create new prototypes with the :func:`make_prototype` factory
107function:
108
109>>> from minihtml import make_prototype
110>>> custom_element = make_prototype("custom-element")
111>>> custom_element
112<PrototypeNonEmpty custom-element>
113
114>>> elem = custom_element()
115>>> print(elem)
116<custom-element></custom-element>
117
118.. _elements:
119
120Elements
121--------
122
123Elements provide the same APIs as :ref:`prototypes`, but they are mutable.
124Calling, indexing or using an element as a context manager returns the element
125itself, so all operations can be chained to modify the element further:
126
127>>> from minihtml.tags import a
128>>> elem = a["repo"](href="https://github.com/trendels/minihtml")("minihtml")
129>>> print(elem)
130<a class="repo" href="https://github.com/trendels/minihtml">minihtml</a>
131
132>>> with html(lang="en") as elem:
133... with div["#main"]:
134... p("content")
135<...>
136>>> print(elem)
137<html lang="en">
138 <div id="main">
139 <p>content</p>
140 </div>
141</html>
142
143.. _attributes:
144
145Attributes
146----------
147
148When setting attributes via keyword arguments, the names of keyword arguments
149are modified according to these rules:
150
151- If the name is ``_`` (a single underscore), it is not changed.
152- Any trailing underscores are stripped from the name.
153- Any remaining underscores are converted to hyphens (``-``).
154
155Examples:
156
157>>> print(div(_="something"))
158<div _="something"></div>
159
160>>> print(div(foo_bar="x"))
161<div foo-bar="x"></div>
162
163>>> from minihtml.tags import label
164>>> print(label(for_="target")("Label"))
165<label for="target">Label</label>
166
167>>> print(div(__foo="bar"))
168<div --foo="bar"></div>
169
170Attribute values are quoted (HTML-escaped) automatically:
171
172>>> print(div(attr='a value with "quotes" and a <tag>'))
173<div attr="a value with "quotes" and a <tag>"></div>
174
175Boolean attributes
176^^^^^^^^^^^^^^^^^^
177
178A boolean HTML attribute is one that does not have a value. For example, to
179mark an input field as required, you can write ``<input required>``.
180
181To set a boolean attribute, pass ``True`` as the value. ``False`` is also a
182valid value, and causes the attribute to be omitted (this can be useful if you
183set the attribute from a variable):
184
185>>> from minihtml.tags import input
186>>> print(input(required=True, disabled=False))
187<input required>
188
189.. _content:
190
191Content
192-------
193
194We have already seen that elements and strings can be passed to elements as content.
195
196String content is also HTML-escaped automatically. To include strings as HTML,
197use the :func:`safe` helper function. Only use this if the content comes from a
198trusted source, to prevent `Cross Site Scripting (XSS) vulnerabilities
199<https://owasp.org/www-community/attacks/xss/>`_!
200
201>>> print(div("content with a <tag>"))
202<div>content with a <tag></div>
203
204>>> from minihtml import safe
205>>> print(div(safe("<b>inline html</b>")))
206<div><b>inline html</b></div>
207
208To add text content to the parent element inside an element context, you can use the
209:func:`text` helper function (:func:`safe` can be used in the same way):
210
211>>> from minihtml import text
212>>> with div as elem:
213... text("text")
214... text(" and more text")
215<...>
216>>> print(elem)
217<div>text and more text</div>
218
219Sometimes you might need to pass around a group of elements that do not share a
220common parent element. This is called a *Fragment*. Fragments are created using
221the :func:`fragment` function:
222
223>>> from minihtml import fragment
224>>> f = fragment(p("one"), p("two"), "three")
225>>> print(f)
226<p>one</p>
227<p>two</p>
228three
229
230:func:`fragment` can also be used in an element context:
231
232>>> with div as elem:
233... fragment(p("one"), p("two"), "three")
234<...>
235>>> print(elem)
236<div>
237 <p>one</p>
238 <p>two</p>
239 three
240</div>