77
88from markupsafe import Markup
99
10+ from .classnames import classnames
1011from .nodes import Element , Fragment , HasHTMLDunder , Node , Text
1112from .parser import parse_html_iter
1213from .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+
90151def _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-
175215def _substitute_attrs (
176216 attrs : dict [str , str | None ], interpolations : tuple [Interpolation , ...]
177217) -> dict [str , str | None ]:
0 commit comments