import html from typing import ( Callable, Union, ) try: from typing import TypeAlias # type: ignore except ImportError: from typing import Any as TypeAlias # type: ignore HTML: TypeAlias = Union[tuple, list['HTML'], Callable[[], str], None, str, int, float] standalones = ['hr', 'br', 'meta'] """Tags which can't have a closing tag.""" def _render_document(document: HTML) -> str: 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_document(b) for b in body) else: items = '' return start + f'{items}' elif callable(document): return str(document()) elif isinstance(document, list): return ''.join(_render_document(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 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 """ return '\n' + _render_document(document)