Skip to content

Commit 98b4265

Browse files
committed
use lxml for to html str
1 parent 6005520 commit 98b4265

File tree

2 files changed

+57
-48
lines changed

2 files changed

+57
-48
lines changed

src/idom/utils.py

+39-40
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from __future__ import annotations
22

33
import re
4-
from collections.abc import Mapping
5-
from html import escape as html_escape
64
from itertools import chain
75
from typing import Any, Callable, Generic, Iterable, TypeVar, cast
8-
from warnings import warn
96

107
from lxml import etree
11-
from lxml.html import fragments_fromstring
8+
from lxml.html import fragments_fromstring, tostring
129

1310
import idom
1411
from 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+
224219
def _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)

tests/test_utils.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -170,23 +170,33 @@ def test_html_to_vdom_with_no_parent_node():
170170
),
171171
(
172172
html.div({"someAttribute": SOME_OBJECT}),
173-
f'<div someattribute="{html_escape(str(SOME_OBJECT))}" />',
173+
f'<div someattribute="{html_escape(str(SOME_OBJECT))}"></div>',
174174
),
175175
(
176-
html.div("hello", html.a({"href": "https://example.com"}, "example")),
177-
'<div>hello<a href="https://example.com">example</a></div>',
176+
html.div(
177+
"hello", html.a({"href": "https://example.com"}, "example"), "world"
178+
),
179+
'<div>hello<a href="https://example.com">example</a>world</div>',
178180
),
179181
(
180182
html.button({"onClick": lambda event: None}),
181-
"<button/>",
183+
"<button></button>",
184+
),
185+
(
186+
html._("hello ", html._("world")),
187+
"hello world",
188+
),
189+
(
190+
html._(html.div("hello"), html._("world")),
191+
"<div>hello</div>world",
182192
),
183193
(
184194
html.div({"style": {"backgroundColor": "blue", "marginLeft": "10px"}}),
185-
'<div style="background-color:blue;margin-left:10px" />',
195+
'<div style="background-color:blue;margin-left:10px"></div>',
186196
),
187197
(
188198
html.div({"style": "background-color:blue;margin-left:10px"}),
189-
'<div style="background-color:blue;margin-left:10px" />',
199+
'<div style="background-color:blue;margin-left:10px"></div>',
190200
),
191201
(
192202
html._(
@@ -203,13 +213,13 @@ def test_html_to_vdom_with_no_parent_node():
203213
),
204214
html.button(),
205215
),
206-
'<div><div>hello</div><a href="https://example.com">example</a><button/></div>',
216+
'<div><div>hello</div><a href="https://example.com">example</a><button></button></div>',
207217
),
208218
(
209219
html.div(
210220
{"dataSomething": 1, "dataSomethingElse": 2, "dataisnotdashed": 3}
211221
),
212-
'<div data-something="1" data-something-else="2" dataisnotdashed="3" />',
222+
'<div data-something="1" data-something-else="2" dataisnotdashed="3"></div>',
213223
),
214224
],
215225
)

0 commit comments

Comments
 (0)