Source code for rational.rendering.builder
"""``h()``: the low-level element builder every primitive is made from."""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from ..events import ClientOp
from .nodes import EVENT_NAMES, AttrValue, Node, TextNode, flatten
if TYPE_CHECKING:
from ..styling.style import Style
EventValue = Callable[..., Any] | ClientOp | list[Callable[..., Any] | ClientOp] | None
def attr_name(py_name: str) -> str:
"""``class_`` -> ``class``, ``aria_label`` -> ``aria-label``, ``data_x`` -> ``data-x``."""
return py_name.rstrip("_").replace("_", "-")
[docs]
def h(
tag: str,
*children: Any,
key: str | int | None = None,
style: Style | None = None,
**kwargs: Any,
) -> Node:
"""Build an element.
``on_<event>=`` accepts a server handler, a ``js.*`` client operation or a
list mixing both. Every other keyword is an HTML attribute (see ``attr_name``).
"""
attrs: list[tuple[str, AttrValue]] = []
events: list[tuple[str, Callable[..., Any]]] = []
client: list[tuple[str, tuple[ClientOp, ...]]] = []
for py_name, value in kwargs.items():
if py_name.startswith("on_"):
event = py_name[3:]
if event not in EVENT_NAMES:
raise ValueError(f"unsupported event {event!r}; expected one of {sorted(EVENT_NAMES)}")
handlers, ops = split_event_value(value)
events.extend((event, fn) for fn in handlers)
if ops:
client.append((event, tuple(ops)))
continue
attrs.append((attr_name(py_name), value))
return Node(
tag,
attrs=tuple(attrs),
children=tuple(normalize_children(children)),
key=None if key is None else str(key),
style=style,
events=tuple(events),
client=tuple(client),
)
def split_event_value(value: EventValue) -> tuple[list[Callable[..., Any]], list[ClientOp]]:
items = value if isinstance(value, list) else [value]
handlers: list[Callable[..., Any]] = []
ops: list[ClientOp] = []
for item in items:
if item is None:
continue
if isinstance(item, ClientOp):
ops.append(item)
elif callable(item):
handlers.append(item)
else:
raise TypeError(f"event handlers must be callables or js.* operations, got {item!r}")
if len(handlers) > 1:
raise ValueError("only one server handler per event; combine them in one method")
return handlers, ops
def normalize_children(children: Any) -> list[Any]:
out = []
for child in flatten(children):
if isinstance(child, str):
out.append(TextNode(child))
elif callable(child) and not hasattr(child, "render"):
out.extend(normalize_children([child()]))
else:
out.append(child)
return out