diff --git a/ludic/attrs.py b/ludic/attrs.py index 6f17884..422a67f 100644 --- a/ludic/attrs.py +++ b/ludic/attrs.py @@ -119,6 +119,13 @@ class HtmxAttrs(Attrs, total=False): hx_ws: Annotated[str, Alias("hx-ws")] hx_sse: Annotated[str, Alias("hx-sse")] + # Extensions + ws_connect: Annotated[str, Alias("ws-connect")] + ws_send: Annotated[str, Alias("ws-send")] + sse_connect: Annotated[str, Alias("sse-connect")] + sse_send: Annotated[str, Alias("sse-send")] + sse_swap: Annotated[str, Alias("sse-swap")] + class WindowEventAttrs(Attrs, total=False): """Event Attributes for HTML elements.""" diff --git a/ludic/format.py b/ludic/format.py index 725192c..8574f48 100644 --- a/ludic/format.py +++ b/ludic/format.py @@ -1,16 +1,34 @@ import html +import inspect import random import re from collections.abc import Mapping from contextvars import ContextVar -from typing import Any, Final, TypeVar +from functools import lru_cache +from typing import Any, Final, TypeVar, get_type_hints T = TypeVar("T") _EXTRACT_NUMBER_RE: Final[re.Pattern[str]] = re.compile(r"\{(\d+:id)\}") -_ALIASES: Final[dict[str, str]] = { - "classes": "class", -} + + +@lru_cache +def _load_attrs_aliases() -> Mapping[str, str]: + from ludic import attrs + + result = {} + for name, cls in inspect.getmembers(attrs, inspect.isclass): + if not name.endswith("Attrs"): + continue + + hints = get_type_hints(cls, include_extras=True) + for key, value in hints.items(): + if metadata := getattr(value, "__metadata__", None): + for meta in metadata: + if isinstance(meta, attrs.Alias): + result[key] = str(meta) + + return result def format_attr_value(key: str, value: Any, is_html: bool = False) -> str: @@ -67,13 +85,13 @@ def format_attrs(attrs: Mapping[str, Any], is_html: bool = False) -> dict[str, A Returns: dict[str, Any]: The formatted attributes. """ + aliases = _load_attrs_aliases() result: dict[str, str] = {} + for key, value in attrs.items(): if formatted_value := format_attr_value(key, value, is_html=is_html): - if key in _ALIASES: - alias = _ALIASES[key] - elif key.startswith("on_"): - alias = key.replace("on_", "on") + if key in aliases: + alias = aliases[key] else: alias = key.strip("_").replace("_", "-") diff --git a/tests/test_elements.py b/tests/test_elements.py index 075d95a..01872fd 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -161,6 +161,17 @@ def test_data_attributes() -> None: assert dom.to_html() == '
content
' +def test_htmx_attributes() -> None: + assert html.button( + "Get Info!", + hx_get="/info", hx_on__before_request="alert('Making a request!')", + ).to_html() == ( # type: ignore + '" + ) # fmt: skip + + def test_all_elements() -> None: assert html.div("test", id="div").to_html() == '
test
' assert html.span("test", id="span").to_html() == 'test' diff --git a/tests/test_formatting.py b/tests/test_formatting.py index f9ea0d5..e4b986b 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -104,3 +104,16 @@ def test_attributes() -> None: assert format_attrs({"class_": "a b c", "classes": ["more", "classes"]}) == { "class": "a b c more classes" } + + assert format_attrs( + { + "hx_on_htmx_before_request": "alert('test')", + "hx_on__after_request": "alert('test2')", + } + ) == { + "hx-on-htmx-before-request": "alert('test')", + "hx-on--after-request": "alert('test2')", + } + assert format_attrs( + {"hx-on:htmx:before-request": "alert('Making a request!')"} + ) == {"hx-on:htmx:before-request": "alert('Making a request!')"}