@@ -62,7 +62,7 @@ def __repr__(self) -> str:
62
62
return f"{ type (self ).__name__ } ({ current } )"
63
63
64
64
65
- def vdom_to_html (value : str | VdomDict , indent : int = 0 , depth : int = 0 ) -> str :
65
+ def vdom_to_html (value : str | VdomDict ) -> str :
66
66
"""Convert a VDOM dictionary into an HTML string
67
67
68
68
Only the following keys are translated to HTML:
@@ -71,80 +71,40 @@ def vdom_to_html(value: str | VdomDict, indent: int = 0, depth: int = 0) -> str:
71
71
- ``attributes``
72
72
- ``children`` (must be strings or more VDOM dicts)
73
73
"""
74
- if indent :
75
- close_indent = f"\n { ' ' * (indent * depth )} "
76
- open_indent = close_indent if depth else close_indent [1 :]
77
- else :
78
- open_indent = close_indent = ""
79
74
80
75
if isinstance (value , str ):
81
- return f" { open_indent } { value } " if depth else value
76
+ return value
82
77
83
78
try :
84
79
tag = value ["tagName" ]
85
80
except TypeError as error : # pragma: no cover
86
81
raise TypeError (f"Expected a VDOM dictionary or string, not { value } " ) from error
87
82
88
- if "attributes" in value :
89
- if not tag : # pragma: no cover
90
- warn ("Ignored attributes from element frament" , UserWarning )
91
- else :
92
- vdom_attributes = dict (value ["attributes" ])
93
- if "style" in vdom_attributes :
94
- vdom_attributes ["style" ] = _vdom_to_html_style (vdom_attributes ["style" ])
95
- for k , v in list (vdom_attributes .items ()):
96
- if not isinstance (v , (str , int )):
97
- del vdom_attributes [k ]
98
- warn (
99
- f"Could not convert attribute of type { type (v ).__name__ } to HTML attribute - { v } " ,
100
- UserWarning ,
101
- )
102
- attributes = (
103
- f""" { ' ' .join (f'{ k } ="{ html_escape (v )} "' for k , v in vdom_attributes .items ())} """
104
- if vdom_attributes
105
- else ""
106
- )
107
- else :
108
- attributes = ""
109
-
110
- if "children" in value :
111
- children_list : list [str ] = []
112
-
113
- for child in value ["children" ]:
114
- if isinstance (child , (dict , str )):
115
- children_list .append (
116
- vdom_to_html (cast ("VdomDict | str" , child ), indent , depth + 1 )
117
- )
118
- else :
119
- warn (
120
- f"Could not convert element of type { type (child ).__name__ !r} to HTML - { child } " ,
121
- UserWarning ,
122
- )
83
+ attributes = " " .join (
84
+ _vdom_to_html_attr (k , v ) for k , v in value .get ("attributes" , {}).items ()
85
+ )
123
86
124
- children = "" .join (children_list )
87
+ if attributes :
88
+ assert tag , "Element frament may not contain attributes"
89
+ attributes = f" { attributes } "
125
90
126
- else :
127
- children = ""
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
+ )
128
97
129
- if not children :
130
- return f" { open_indent } < { tag } { attributes } />" if tag else ""
131
- else :
132
- return (
133
- f" { open_indent } < { tag } { attributes } > { children } { close_indent } </ { tag } >"
134
- if tag
135
- else children
98
+ 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 } />" )
136
105
)
137
-
138
-
139
- _CAMEL_CASE_SUB_PATTERN = re .compile (r"(?<!^)(?=[A-Z])" )
140
-
141
-
142
- def _vdom_to_html_style (style : str | dict [str , Any ]) -> str :
143
- if not isinstance (style , Mapping ):
144
- return style
145
-
146
- return ";" .join (
147
- f"{ _CAMEL_CASE_SUB_PATTERN .sub ('-' , k ).lower ()} :{ v } " for k , v in style .items ()
106
+ if tag
107
+ else children
148
108
)
149
109
150
110
@@ -210,6 +170,10 @@ def html_to_vdom(
210
170
return vdom
211
171
212
172
173
+ class HTMLParseError (etree .LxmlSyntaxError ): # type: ignore[misc]
174
+ """Raised when an HTML document cannot be parsed using strict parsing."""
175
+
176
+
213
177
def _etree_to_vdom (
214
178
node : etree ._Element , transforms : Iterable [_ModelTransform ]
215
179
) -> VdomDict :
@@ -313,5 +277,32 @@ def _hypen_to_camel_case(string: str) -> str:
313
277
return first .lower () + remainder .title ().replace ("-" , "" )
314
278
315
279
316
- class HTMLParseError (etree .LxmlSyntaxError ): # type: ignore[misc]
317
- """Raised when an HTML document cannot be parsed using strict parsing."""
280
+ # Pattern for delimitting camelCase names (e.g. camelCase to camel-case)
281
+ _CAMEL_CASE_SUB_PATTERN = re .compile (r"(?<!^)(?=[A-Z])" )
282
+
283
+ # see list of HTML attributes with dashes in them:
284
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#attribute_list
285
+ _CAMEL_TO_DASH_CASE_HTML_ATTRS = {
286
+ "acceptCharset" : "accept-charset" ,
287
+ "httpEquiv" : "http-equiv" ,
288
+ }
289
+
290
+
291
+ def _vdom_to_html_attr (key : str , value : Any ) -> str :
292
+ if key == "style" :
293
+ if isinstance (value , dict ):
294
+ value = ";" .join (
295
+ # We lower only to normalize - CSS is case-insensitive:
296
+ # https://www.w3.org/TR/css-fonts-3/#font-family-casing
297
+ f"{ _CAMEL_CASE_SUB_PATTERN .sub ('-' , k ).lower ()} :{ v } "
298
+ for k , v in value .items ()
299
+ )
300
+ elif key .startswith ("data" ):
301
+ # Handle data-* attribute names
302
+ key = _CAMEL_CASE_SUB_PATTERN .sub ("-" , key )
303
+ else :
304
+ key = _CAMEL_TO_DASH_CASE_HTML_ATTRS .get (key , key )
305
+
306
+ # Again, we lower the attribute name only to normalize - HTML is case-insensitive:
307
+ # http://w3c.github.io/html-reference/documents.html#case-insensitivity
308
+ return f'{ key .lower ()} ="{ html_escape (str (value ))} "'
0 commit comments