"""
Provide the HTML API, for generating HTML pages.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import MutableMapping, MutableSequence, Sequence
from inspect import getmembers
from typing import (
Any,
Generic,
NotRequired,
Self,
TypedDict,
TypeVar,
Unpack,
cast,
final,
overload,
)
from typing_extensions import override
from betty.string import (
kebab_case_to_lower_camel_case,
kebab_case_to_snake_case,
snake_case_to_kebab_case,
)
_AttributeGetT = TypeVar("_AttributeGetT")
_AttributeSetT = TypeVar("_AttributeSetT")
class _Attribute(Generic[_AttributeGetT, _AttributeSetT], ABC):
def __init__(self, html_name: str):
self._html_name = html_name
self._attr_name = f"_{kebab_case_to_lower_camel_case(html_name)}"
@overload
def __get__(self, instance: None, owner: type[Attributes]) -> Self:
pass
@overload
def __get__(self, instance: Attributes, owner: type[Attributes]) -> _AttributeGetT:
pass
def __get__(
self, instance: Attributes | None, owner: type[Attributes]
) -> _AttributeGetT | Self:
if instance is None:
return self # type: ignore[return-value]
return self.get(instance)
def get(self, instance: Attributes) -> _AttributeGetT:
try:
return cast(_AttributeGetT, getattr(instance, self._attr_name))
except AttributeError:
value = self._new_default()
setattr(instance, self._attr_name, value)
return value
def __set__(self, instance: Attributes, value: _AttributeSetT) -> None:
self.set(instance, value)
@abstractmethod
def set(self, instance: Attributes, value: _AttributeSetT) -> None:
pass
def setdefault(self, instance: Attributes, value: _AttributeSetT) -> None:
if getattr(instance, self._attr_name, None):
return
self.set(instance, value)
@abstractmethod
def _new_default(self) -> _AttributeGetT:
pass
def format(self, instance: Attributes) -> str:
"""
Format the attribute to a string.
"""
value = self.get(instance)
if value:
return self._format_value(value)
return ""
@abstractmethod
def _format_value(self, value: _AttributeGetT) -> str:
pass
class _BooleanAttribute(_Attribute[bool, bool]):
@override
def set(self, instance: Attributes, value: bool) -> None:
setattr(instance, self._attr_name, value)
@override
def _format_value(self, value: bool) -> str:
return self._html_name
@override
def _new_default(self) -> bool:
return False
class _StringAttribute(_Attribute[str, str]):
@override
def set(self, instance: Attributes, value: str) -> None:
setattr(instance, self._attr_name, value)
@override
def _format_value(self, value: str) -> str:
return f'{self._html_name}="{value}"'
@override
def _new_default(self) -> str:
return ""
class _MultipleStringAttribute(_Attribute[MutableSequence[str], Sequence[str]]):
def __init__(self, html_name: str, separator: str = " "):
super().__init__(html_name)
self._separator = separator
@override
def set(self, instance: Attributes, value: Sequence[str]) -> None:
sequence = self.get(instance)
sequence.clear()
sequence.extend(value)
@override
def _format_value(self, value: Sequence[str]) -> str:
return f'{self._html_name}="{self._separator.join(value)}"'
@override
def _new_default(self) -> MutableSequence[str]:
return []
class _BooleanOrStringAttribute(_Attribute[bool | str, bool | str]):
@override
def set(self, instance: Attributes, value: bool | str) -> None:
setattr(instance, self._attr_name, value)
@override
def _format_value(self, value: bool | str) -> str:
if isinstance(value, bool):
return self._html_name
return f'{self._html_name}="{value}"'
@override
def _new_default(self) -> bool | str:
return False
class _AttributesKwargs(TypedDict):
html_accept: NotRequired[Sequence[str]]
html_accept_charset: NotRequired[str]
html_accesskey: NotRequired[str]
html_action: NotRequired[str]
html_allow: NotRequired[str]
html_alt: NotRequired[str]
html_aria_controls: NotRequired[Sequence[str]]
html_aria_expanded: NotRequired[bool]
html_as: NotRequired[str]
html_async: NotRequired[bool]
html_autocapitalize: NotRequired[str]
html_autocomplete: NotRequired[str]
html_autoplay: NotRequired[bool]
html_capture: NotRequired[str]
html_charset: NotRequired[str]
html_checked: NotRequired[bool]
html_cite: NotRequired[str]
html_class: NotRequired[Sequence[str]]
html_cols: NotRequired[str]
html_colspan: NotRequired[str]
html_content: NotRequired[str]
html_contenteditable: NotRequired[str]
html_controls: NotRequired[bool]
html_coords: NotRequired[str]
html_crossorigin: NotRequired[str]
html_data: NotRequired[str]
html_datetime: NotRequired[str]
html_decoding: NotRequired[str]
html_default: NotRequired[bool]
html_defer: NotRequired[bool]
html_dir: NotRequired[str]
html_dirname: NotRequired[str]
html_disabled: NotRequired[bool]
html_download: NotRequired[bool | str]
html_draggable: NotRequired[str]
html_enctype: NotRequired[str]
html_enterkeyhint: NotRequired[str]
html_for: NotRequired[str]
html_formaction: NotRequired[str]
html_formenctype: NotRequired[str]
html_formmethod: NotRequired[str]
html_formnovalidate: NotRequired[bool]
html_formtarget: NotRequired[str]
html_headers: NotRequired[Sequence[str]]
html_height: NotRequired[str]
html_hidden: NotRequired[str]
html_high: NotRequired[str]
html_href: NotRequired[str]
html_hreflang: NotRequired[str]
html_http_equiv: NotRequired[str]
html_id: NotRequired[str]
html_integrity: NotRequired[str]
html_inputmode: NotRequired[str]
html_ismap: NotRequired[bool]
html_itemprop: NotRequired[str]
html_kind: NotRequired[str]
html_label: NotRequired[str]
html_lang: NotRequired[str]
html_loading: NotRequired[str]
html_list: NotRequired[str]
html_loop: NotRequired[bool]
html_low: NotRequired[str]
html_max: NotRequired[str]
html_maxlength: NotRequired[str]
html_minlength: NotRequired[str]
html_media: NotRequired[str]
html_method: NotRequired[str]
html_min: NotRequired[str]
html_multiple: NotRequired[bool]
html_muted: NotRequired[bool]
html_name: NotRequired[str]
html_novalidate: NotRequired[bool]
html_open: NotRequired[bool]
html_optimum: NotRequired[str]
html_pattern: NotRequired[str]
html_ping: NotRequired[Sequence[str]]
html_placeholder: NotRequired[str]
html_playsinline: NotRequired[bool]
html_poster: NotRequired[str]
html_preload: NotRequired[str]
html_readonly: NotRequired[bool]
html_referrerpolicy: NotRequired[str]
html_rel: NotRequired[str]
html_required: NotRequired[bool]
html_reversed: NotRequired[bool]
html_role: NotRequired[str]
html_rows: NotRequired[str]
html_rowspan: NotRequired[str]
html_sandbox: NotRequired[Sequence[str]]
html_scope: NotRequired[str]
html_selected: NotRequired[bool]
html_shape: NotRequired[str]
html_size: NotRequired[str]
html_sizes: NotRequired[str]
html_slot: NotRequired[str]
html_span: NotRequired[str]
html_spellcheck: NotRequired[str]
html_src: NotRequired[str]
html_srcdoc: NotRequired[str]
html_srclang: NotRequired[str]
html_srcset: NotRequired[Sequence[str]]
html_start: NotRequired[str]
html_step: NotRequired[str]
html_style: NotRequired[str]
html_tabindex: NotRequired[str]
html_target: NotRequired[str]
html_title: NotRequired[str]
html_translate: NotRequired[str]
html_type: NotRequired[str]
html_usemap: NotRequired[str]
html_value: NotRequired[str]
html_width: NotRequired[str]
html_wrap: NotRequired[str]
[docs]
@final
class Attributes:
"""
Manage attributes for an HTML element.
"""
# Based on https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes.
html_accept = _MultipleStringAttribute("accept", ", ")
html_accept_charset = _StringAttribute("accept-charset")
html_accesskey = _StringAttribute("accesskey")
html_action = _StringAttribute("action")
html_allow = _StringAttribute("allow")
html_alt = _StringAttribute("alt")
html_aria_controls = _MultipleStringAttribute("aria-controls")
html_aria_expanded = _BooleanAttribute("aria-expanded")
html_as = _StringAttribute("as")
html_async = _BooleanAttribute("async")
html_autocapitalize = _StringAttribute("autocapitalize")
html_autocomplete = _StringAttribute("autocomplete")
html_autoplay = _BooleanAttribute("autoplay")
html_capture = _StringAttribute("capture")
html_charset = _StringAttribute("charset")
html_checked = _BooleanAttribute("checked")
html_cite = _StringAttribute("cite")
html_class = _MultipleStringAttribute("class")
html_cols = _StringAttribute("cols")
html_colspan = _StringAttribute("colspan")
html_content = _StringAttribute("content")
html_contenteditable = _StringAttribute("contenteditable")
html_controls = _BooleanAttribute("controls")
html_coords = _StringAttribute("coords")
html_crossorigin = _StringAttribute("crossorigin")
# @todo "csp" is not yet documented by MDN.
html_data = _StringAttribute("data")
html_datetime = _StringAttribute("datetime")
html_decoding = _StringAttribute("decoding")
html_default = _BooleanAttribute("default")
html_defer = _BooleanAttribute("defer")
html_dir = _StringAttribute("dir")
html_dirname = _StringAttribute("dirname")
html_disabled = _BooleanAttribute("disabled")
html_download = _BooleanOrStringAttribute("download")
html_draggable = _StringAttribute("draggable")
html_enctype = _StringAttribute("enctype")
html_enterkeyhint = _StringAttribute("enterkeyhint")
html_for = _StringAttribute("for")
# @todo "form" is not yet documented by MDN.
html_formaction = _StringAttribute("formaction")
html_formenctype = _StringAttribute("formenctype")
html_formmethod = _StringAttribute("formmethod")
html_formnovalidate = _BooleanAttribute("formnovalidate")
html_formtarget = _StringAttribute("formtarget")
html_headers = _MultipleStringAttribute("headers")
html_height = _StringAttribute("height")
html_hidden = _StringAttribute("hidden")
html_high = _StringAttribute("high")
html_href = _StringAttribute("href")
html_hreflang = _StringAttribute("hreflang")
html_http_equiv = _StringAttribute("http-equiv")
html_id = _StringAttribute("id")
html_integrity = _StringAttribute("integrity")
html_inputmode = _StringAttribute("inputmode")
html_ismap = _BooleanAttribute("ismap")
html_itemprop = _StringAttribute("itemprop")
html_kind = _StringAttribute("kind")
html_label = _StringAttribute("label")
html_lang = _StringAttribute("lang")
html_loading = _StringAttribute("loading")
html_list = _StringAttribute("list")
html_loop = _BooleanAttribute("loop")
html_low = _StringAttribute("low")
html_max = _StringAttribute("max")
html_maxlength = _StringAttribute("maxlength")
html_minlength = _StringAttribute("minlength")
html_media = _StringAttribute("media")
html_method = _StringAttribute("method")
html_min = _StringAttribute("min")
html_multiple = _BooleanAttribute("multiple")
html_muted = _BooleanAttribute("muted")
html_name = _StringAttribute("name")
html_novalidate = _BooleanAttribute("novalidate")
html_open = _BooleanAttribute("open")
html_optimum = _StringAttribute("optimum")
html_pattern = _StringAttribute("pattern")
html_ping = _MultipleStringAttribute("ping")
html_placeholder = _StringAttribute("placeholder")
html_playsinline = _BooleanAttribute("playsinline")
html_poster = _StringAttribute("poster")
html_preload = _StringAttribute("preload")
html_readonly = _BooleanAttribute("readonly")
html_referrerpolicy = _StringAttribute("referrerpolicy")
html_rel = _StringAttribute("rel")
html_required = _BooleanAttribute("required")
html_reversed = _BooleanAttribute("reversed")
html_role = _StringAttribute("role")
html_rows = _StringAttribute("rows")
html_rowspan = _StringAttribute("rowspan")
html_sandbox = _MultipleStringAttribute("sandbox")
html_scope = _StringAttribute("scope")
html_selected = _BooleanAttribute("selected")
html_shape = _StringAttribute("shape")
html_size = _StringAttribute("size")
html_sizes = _StringAttribute("sizes")
html_slot = _StringAttribute("slot")
html_span = _StringAttribute("span")
html_spellcheck = _StringAttribute("spellcheck")
html_src = _StringAttribute("src")
html_srcdoc = _StringAttribute("srcdoc")
html_srclang = _StringAttribute("srclang")
html_srcset = _MultipleStringAttribute("srcset", ", ")
html_start = _StringAttribute("start")
html_step = _StringAttribute("step")
html_style = _StringAttribute("style")
html_tabindex = _StringAttribute("tabindex")
html_target = _StringAttribute("target")
html_title = _StringAttribute("title")
html_translate = _StringAttribute("translate")
html_type = _StringAttribute("type")
html_usemap = _StringAttribute("usemap")
html_value = _StringAttribute("value")
html_width = _StringAttribute("width")
html_wrap = _StringAttribute("wrap")
[docs]
def __init__(self, **kwargs: Unpack[_AttributesKwargs]):
self._data_attributes: MutableMapping[str, str] = {}
self.set(**kwargs)
def _get_attribute(self, attr_name: str) -> _Attribute[Any, Any]:
return cast(_Attribute[Any, Any], getattr(type(self), attr_name))
[docs]
def set(self, **attributes: Unpack[_AttributesKwargs]) -> None:
"""
Set values for the given HTML attributes.
"""
for attribute_name, attribute_value in attributes.items():
self._get_attribute(attribute_name).set(self, attribute_value)
[docs]
def setdefault(self, **attributes: Unpack[_AttributesKwargs]) -> None:
"""
Set values for the given HTML attributes, but only for those attributes that do not already have a value set.
"""
for attribute_name, attribute_value in attributes.items():
self._get_attribute(attribute_name).setdefault(self, attribute_value)
[docs]
def set_data(self, **attributes: str) -> None:
"""
Set values for the given HTML data attributes.
"""
self._data_attributes.update(attributes)
[docs]
def get_data(self, attribute_name: str) -> str | None:
"""
Get the value for the given HTML data attribute.
"""
try:
return self._data_attributes[kebab_case_to_snake_case(attribute_name)]
except KeyError:
return None
def __str__(self) -> str:
return self.format()
def __html__(self) -> str:
return self.format()