@@ -87,20 +87,109 @@ def _instrument_and_parse(strings: tuple[str, ...]) -> Node:
8787# --------------------------------------------------------------------------
8888
8989
90+ def _substitute_attr (
91+ key : str ,
92+ value : object ,
93+ ) -> t .Iterable [tuple [str , str | None ]]:
94+ """
95+ Substitute a single attribute based on its key and the interpolated value.
96+
97+ A single parsed attribute with a placeholder may result in multiple
98+ attributes in the final output, for instance if the value is a dict or
99+ iterable of key-value pairs. Likewise, a value of False will result in
100+ the attribute being omitted entirely; nothing is yielded in that case.
101+ """
102+ match value :
103+ case str ():
104+ yield (key , str (value ))
105+ case True :
106+ yield (key , None )
107+ case False | None :
108+ pass
109+ case dict () as d :
110+ for sub_k , sub_v in d .items ():
111+ if sub_v is True :
112+ yield sub_k , None
113+ elif sub_v not in (False , None ):
114+ yield sub_k , str (sub_v )
115+ case Iterable () as it :
116+ for item in it :
117+ match item :
118+ case tuple () if len (item ) == 2 :
119+ sub_k , sub_v = item
120+ if sub_v is True :
121+ yield sub_k , None
122+ elif sub_v not in (False , None ):
123+ yield sub_k , str (sub_v )
124+ case str () | Markup ():
125+ yield str (item ), None
126+ case _:
127+ raise TypeError (
128+ f"Cannot use { type (item ).__name__ } as attribute "
129+ f"key-value pair in iterable for attribute '{ key } '"
130+ )
131+ case _:
132+ raise TypeError (
133+ f"Cannot use { type (value ).__name__ } as attribute value for "
134+ f"attribute '{ key } '"
135+ )
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+ 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+
90175def _substitute_attrs (
91176 attrs : dict [str , str | None ], interpolations : tuple [Interpolation , ...]
92177) -> dict [str , str | None ]:
178+ """Substitute placeholders in attributes based on the corresponding interpolations."""
93179 new_attrs : dict [str , str | None ] = {}
94180 for key , value in attrs .items ():
95- if key .startswith (_PLACEHOLDER_PREFIX ):
181+ if value and value .startswith (_PLACEHOLDER_PREFIX ):
182+ index = _placholder_index (value )
183+ interpolation = interpolations [index ]
184+ value = format_interpolation (interpolation )
185+ for sub_k , sub_v in _substitute_attr (key , value ):
186+ new_attrs [sub_k ] = sub_v
187+ elif key .startswith (_PLACEHOLDER_PREFIX ):
96188 index = _placholder_index (key )
97189 interpolation = interpolations [index ]
98190 value = format_interpolation (interpolation )
99- if not isinstance (value , str ):
100- raise ValueError (
101- f"Attribute interpolation must be a string, got: { value !r} "
102- )
103- new_attrs [key ] = value
191+ for sub_k , sub_v in _substitute_spread_attrs (value ):
192+ new_attrs [sub_k ] = sub_v
104193 else :
105194 new_attrs [key ] = value
106195 return new_attrs
@@ -123,38 +212,38 @@ def _substitute_and_flatten_children(
123212
124213
125214def _node_from_value (value : object ) -> Node :
126- """Convert a value to a Node, if possible."""
127- # This is a bit of a hack, but it lets us handle Markup and
128- # other objects that implement __html__ without special-casing them here.
129- # We use a Text node to wrap the value, then parse it back out.
130- # This is not the most efficient, but it is simple and works.
131- node = Text (_placeholder (0 ))
132- interpolations = (Interpolation (value , "" , None , "" ),)
133- return _substitute_node (node , interpolations )
215+ """
216+ Convert an arbitrary value to a Node.
217+
218+ This is the primary substitution performed when replacing interpolations
219+ in child content positions.
220+ """
221+ match value :
222+ case str ():
223+ return Text (value )
224+ case Node ():
225+ return value
226+ case Template ():
227+ return html (value )
228+ case HasHTMLDunder ():
229+ return Text (value )
230+ case False :
231+ return Text ("" )
232+ case Iterable ():
233+ children = [_node_from_value (v ) for v in value ]
234+ return Fragment (children = children )
235+ case _:
236+ return Text (str (value ))
134237
135238
136239def _substitute_node (p_node : Node , interpolations : tuple [Interpolation , ...]) -> Node :
240+ """Substitute placeholders in a node based on the corresponding interpolations."""
137241 match p_node :
138242 case Text (text ) if str (text ).startswith (_PLACEHOLDER_PREFIX ):
139243 index = _placholder_index (str (text ))
140244 interpolation = interpolations [index ]
141245 value = format_interpolation (interpolation )
142- match value :
143- case str ():
144- return Text (value )
145- case Node ():
146- return value
147- case Template ():
148- return html (value )
149- case HasHTMLDunder ():
150- return Text (value )
151- case False :
152- return Text ("" )
153- case Iterable ():
154- children = [_node_from_value (v ) for v in value ]
155- return Fragment (children = children )
156- case _:
157- return Text (str (value ))
246+ return _node_from_value (value )
158247 case Element (tag = tag , attrs = attrs , children = children ):
159248 new_attrs = _substitute_attrs (attrs , interpolations )
160249 new_children = _substitute_and_flatten_children (children , interpolations )
@@ -172,7 +261,7 @@ def _substitute_node(p_node: Node, interpolations: tuple[Interpolation, ...]) ->
172261
173262
174263def html (template : Template ) -> Node :
175- """Create an HTML element from a string ."""
264+ """Parse a t-string and return a tree of Nodes ."""
176265 # Parse the HTML, returning a tree of nodes with placeholders
177266 # where interpolations go.
178267 p_node = _instrument_and_parse (template .strings )
0 commit comments