11from __future__ import annotations
22
33import re
4- from collections .abc import Mapping
5- from html import escape as html_escape
64from itertools import chain
75from typing import Any , Callable , Generic , Iterable , TypeVar , cast
8- from warnings import warn
96
107from lxml import etree
11- from lxml .html import fragments_fromstring
8+ from lxml .html import fragments_fromstring , tostring
129
1310import idom
1411from idom .core .types import VdomDict
@@ -62,7 +59,7 @@ def __repr__(self) -> str:
6259 return f"{ type (self ).__name__ } ({ current } )"
6360
6461
65- def vdom_to_html (value : str | VdomDict ) -> str :
62+ def vdom_to_html (value : VdomDict ) -> str :
6663 """Convert a VDOM dictionary into an HTML string
6764
6865 Only the following keys are translated to HTML:
@@ -71,40 +68,12 @@ def vdom_to_html(value: str | VdomDict) -> str:
7168 - ``attributes``
7269 - ``children`` (must be strings or more VDOM dicts)
7370 """
74-
75- if isinstance (value , str ):
76- return value
77-
78- try :
79- tag = value ["tagName" ]
80- except TypeError as error : # pragma: no cover
81- raise TypeError (f"Expected a VDOM dictionary or string, not { value } " ) from error
82-
83- attributes = " " .join (
84- _vdom_to_html_attr (k , v ) for k , v in value .get ("attributes" , {}).items ()
85- )
86-
87- if attributes :
88- assert tag , "Element frament may not contain attributes"
89- attributes = f" { attributes } "
90-
91- children = "" .join (
92- vdom_to_html (cast ("VdomDict | str" , c ))
93- if isinstance (c , (dict , str ))
94- else html_escape (str (c ))
95- for c in value .get ("children" , ())
96- )
97-
71+ temp_root = etree .Element ("__temp__" )
72+ _add_vdom_to_etree (temp_root , value )
9873 return (
99- (
100- f"<{ tag } { attributes } >{ children } </{ tag } >"
101- if children
102- # To be safe we mark elements without children as self-closing.
103- # https://html.spec.whatwg.org/multipage/syntax.html#foreign-elements
104- else (f"<{ tag } { attributes } />" if attributes else f"<{ tag } />" )
105- )
106- if tag
107- else children
74+ cast (bytes , tostring (temp_root )).decode ()
75+ # strip out temp root <__temp__> element
76+ [10 :- 11 ]
10877 )
10978
11079
@@ -221,6 +190,32 @@ def _etree_to_vdom(
221190 return vdom
222191
223192
193+ def _add_vdom_to_etree (parent : etree ._Element , vdom : VdomDict ) -> None :
194+ try :
195+ tag = vdom ["tagName" ]
196+ except TypeError as e :
197+ raise TypeError (f"Expected a VdomDict, not { vdom } " ) from e
198+ except KeyError as e :
199+ raise TypeError (f"Expected a VdomDict, not { vdom } " ) from e
200+
201+ if tag :
202+ element = etree .SubElement (parent , tag )
203+ element .attrib .update (
204+ _vdom_to_html_attr (k , v ) for k , v in vdom .get ("attributes" , {}).items ()
205+ )
206+ else :
207+ element = parent
208+
209+ for c in vdom .get ("children" , []):
210+ if isinstance (c , dict ):
211+ _add_vdom_to_etree (element , cast (VdomDict , c ))
212+ elif len (element ):
213+ last_child = element [- 1 ]
214+ last_child .tail = f"{ last_child .tail or '' } { c } "
215+ else :
216+ element .text = f"{ element .text or '' } { c } "
217+
218+
224219def _mutate_vdom (vdom : VdomDict ) -> None :
225220 """Performs any necessary mutations on the VDOM attributes to meet VDOM spec.
226221
@@ -288,7 +283,7 @@ def _hypen_to_camel_case(string: str) -> str:
288283}
289284
290285
291- def _vdom_to_html_attr (key : str , value : Any ) -> str :
286+ def _vdom_to_html_attr (key : str , value : Any ) -> tuple [ str , str ] :
292287 if key == "style" :
293288 if isinstance (value , dict ):
294289 value = ";" .join (
@@ -303,6 +298,10 @@ def _vdom_to_html_attr(key: str, value: Any) -> str:
303298 else :
304299 key = _CAMEL_TO_DASH_CASE_HTML_ATTRS .get (key , key )
305300
301+ assert not callable (
302+ value
303+ ), f"Could not convert callable attribute { key } ={ value } to HTML"
304+
306305 # Again, we lower the attribute name only to normalize - HTML is case-insensitive:
307306 # http://w3c.github.io/html-reference/documents.html#case-insensitivity
308- return f' { key .lower ()} =" { html_escape ( str (value )) } "'
307+ return key .lower (), str (value )
0 commit comments