""" Render python structures into HTML strings. Instead of constructing HTML strings manually or through templates, instead write python structures, and serialize them at the last possible moment. See ``render_document`` for a detailed explanation of valid types. """ import html from typing import ( Any, Callable, Union, ) try: # pragma: no cover from typing import TypeAlias # type: ignore except ImportError: # pragma: no cover from typing import Any as TypeAlias # type: ignore # TODO compare this against xml.etree.ElementTree, which appears to # have a HTML mode. # ``tuple[Any, ...]`` should really be ``tuple['HTML', ...]``, but # that doesn't work for some reason. HTML: TypeAlias = Union[tuple[Any, ...], list['HTML'], Callable[[], str], None, str, int, float] standalones = ['hr', 'br', 'meta'] """Tags which can't have a closing tag.""" def render_fragment(document: HTML) -> str: """ Render an HTML structure to an Html string. The following Python types are converted as follows: - Tuples - The first value becomes the tags name - The second value, if a dictionary, becomes the tags attributes - All following values (including the second if not a dictionary) gets individually passed to render_document. - Lists Each element gets passed to render_document - Callable[[], str] Gets called, and its output is included verbatim. Useful for including strings which shouldn't be escaped. - str Gets escaped, and included - int, float Gets included as their default string representation. - None Becomes an empty string """ if isinstance(document, tuple): tag, *body = document if body and isinstance(body[0], dict): attributes = ' '.join(f'{a}="{html.escape(b)}"' for a, b in body[0].items()) body = body[1:] start = f'<{tag} {attributes}>' else: start = f'<{tag}>' if tag in standalones: return start else: if body: items = ''.join(render_fragment(b) for b in body) else: items = '' return start + f'{items}' elif callable(document): return str(document()) elif isinstance(document, list): return ''.join(render_fragment(e) for e in document) elif document is None: return '' else: # strings, and everything else return html.escape(str(document)) def render_document(document: HTML) -> str: """ Render a complete HTML document. Renders the given root fragment, and prepends a doctype declaration. """ return '\n' + render_fragment(document)