Simple HTML Generation https://minihtml.trendels.name/
at main 240 lines 6.2 kB view raw
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 &quot;quotes&quot; and a &lt;tag&gt;"></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 &lt;tag&gt;</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>