Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 61ae7a7

Browse files
committed
Clean implementation for everything but component invocations.
1 parent d458ca0 commit 61ae7a7

File tree

2 files changed

+79
-39
lines changed

2 files changed

+79
-39
lines changed

html_tstring/processor.py

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from markupsafe import Markup
99

10+
from .classnames import classnames
1011
from .nodes import Element, Fragment, HasHTMLDunder, Node, Text
1112
from .parser import parse_html_iter
1213
from .utils import format_interpolation as base_format_interpolation
@@ -87,6 +88,66 @@ def _instrument_and_parse(strings: tuple[str, ...]) -> Node:
8788
# --------------------------------------------------------------------------
8889

8990

91+
def _force_dict(value: t.Any, *, kind: str) -> dict:
92+
"""Try to convert a value to a dict, raising TypeError if not possible."""
93+
try:
94+
return dict(value)
95+
except (TypeError, ValueError):
96+
raise TypeError(
97+
f"Cannot use {type(value).__name__} as value for {kind} attributes"
98+
) from None
99+
100+
101+
def _substitute_attrs_dict(
102+
value: object, *, kind: str
103+
) -> t.Iterable[tuple[str, str | None]]:
104+
"""Substitute attributes based on the interpolated value being a dict."""
105+
d = _force_dict(value, kind=kind)
106+
for sub_k, sub_v in d.items():
107+
if sub_v is True:
108+
yield f"{kind}-{sub_k}", None
109+
elif sub_v not in (False, None):
110+
yield f"{kind}-{sub_k}", str(sub_v)
111+
112+
113+
def _substitute_aria_attrs(value: object) -> t.Iterable[tuple[str, str | None]]:
114+
"""Produce aria-* attributes based on the interpolated value for "aria"."""
115+
return _substitute_attrs_dict(value, kind="aria")
116+
117+
118+
def _substitute_data_attrs(value: object) -> t.Iterable[tuple[str, str | None]]:
119+
"""Produce data-* attributes based on the interpolated value for "data"."""
120+
return _substitute_attrs_dict(value, kind="data")
121+
122+
123+
def _substitute_class_attr(value: object) -> t.Iterable[tuple[str, str | None]]:
124+
"""Substitute a class attribute based on the interpolated value."""
125+
yield ("class", classnames(value))
126+
127+
128+
def _substitute_style_attr(value: object) -> t.Iterable[tuple[str, str | None]]:
129+
"""Substitute a style attribute based on the interpolated value."""
130+
try:
131+
d = _force_dict(value, kind="style")
132+
style_str = "; ".join(f"{k}: {v}" for k, v in d.items())
133+
yield ("style", style_str)
134+
except TypeError:
135+
yield ("style", str(value))
136+
137+
138+
def _substitute_spread_attrs(value: object) -> t.Iterable[tuple[str, str | None]]:
139+
"""
140+
Substitute a spread attribute based on the interpolated value.
141+
142+
A spread attribute is one where the key is a placeholder, indicating that
143+
the entire attribute set should be replaced by the interpolated value.
144+
The value must be a dict or iterable of key-value pairs.
145+
"""
146+
d = _force_dict(value, kind="spread")
147+
for sub_k, sub_v in d.items():
148+
yield from _substitute_attr(sub_k, sub_v)
149+
150+
90151
def _substitute_attr(
91152
key: str,
92153
value: object,
@@ -99,6 +160,22 @@ def _substitute_attr(
99160
iterable of key-value pairs. Likewise, a value of False will result in
100161
the attribute being omitted entirely; nothing is yielded in that case.
101162
"""
163+
# Special handling for certain attribute names that have special semantics:
164+
match key:
165+
case "class":
166+
yield from _substitute_class_attr(value)
167+
return
168+
case "data":
169+
yield from _substitute_data_attrs(value)
170+
return
171+
case "style":
172+
yield from _substitute_style_attr(value)
173+
return
174+
case "aria":
175+
yield from _substitute_aria_attrs(value)
176+
return
177+
178+
# General handling for all other attributes:
102179
match value:
103180
case str():
104181
yield (key, str(value))
@@ -135,43 +212,6 @@ def _substitute_attr(
135212
)
136213

137214

138-
def _substitute_spread_attrs(value: object) -> t.Iterable[tuple[str, str | None]]:
139-
"""
140-
Substitute a spread attribute based on the interpolated value.
141-
142-
A spread attribute is one where the key is a placeholder, indicating that
143-
the entire attribute set should be replaced by the interpolated value.
144-
The value must be a dict or iterable of key-value pairs.
145-
"""
146-
match value:
147-
case dict() as d:
148-
for sub_k, sub_v in d.items():
149-
if sub_v is True:
150-
yield sub_k, None
151-
elif sub_v not in (False, None):
152-
yield sub_k, str(sub_v)
153-
case Iterable() as it:
154-
for item in it:
155-
match item:
156-
case tuple() if len(item) == 2:
157-
sub_k, sub_v = item
158-
if sub_v is True:
159-
yield sub_k, None
160-
elif sub_v not in (False, None):
161-
yield sub_k, str(sub_v)
162-
case str() | Markup():
163-
yield str(item), None
164-
case _:
165-
raise TypeError(
166-
f"Cannot use {type(item).__name__} as attribute "
167-
f"key-value pair in iterable for spread attribute"
168-
)
169-
case _:
170-
raise TypeError(
171-
f"Cannot use {type(value).__name__} as value for spread attribute"
172-
)
173-
174-
175215
def _substitute_attrs(
176216
attrs: dict[str, str | None], interpolations: tuple[Interpolation, ...]
177217
) -> dict[str, str | None]:

html_tstring/processor_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,10 @@ def test_interpolated_aria_attributes():
430430
node = html(t"<button aria={aria}>X</button>")
431431
assert node == Element(
432432
"button",
433-
attrs={"aria-label": "Close", "aria-hidden": "True"},
433+
attrs={"aria-label": "Close", "aria-hidden": None},
434434
children=[Text("X")],
435435
)
436-
assert str(node) == '<button aria-label="Close" aria-hidden="True">X</button>'
436+
assert str(node) == '<button aria-label="Close" aria-hidden>X</button>'
437437

438438

439439
def test_interpolated_style_attribute():

0 commit comments

Comments
 (0)