Skip to content

Commit ba9ba84

Browse files
committed
remove indent + simplify implementation
1 parent ac2cdec commit ba9ba84

File tree

2 files changed

+67
-81
lines changed

2 files changed

+67
-81
lines changed

src/idom/utils.py

Lines changed: 56 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def __repr__(self) -> str:
6262
return f"{type(self).__name__}({current})"
6363

6464

65-
def vdom_to_html(value: str | VdomDict, indent: int = 0, depth: int = 0) -> str:
65+
def vdom_to_html(value: str | VdomDict) -> str:
6666
"""Convert a VDOM dictionary into an HTML string
6767
6868
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:
7171
- ``attributes``
7272
- ``children`` (must be strings or more VDOM dicts)
7373
"""
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 = ""
7974

8075
if isinstance(value, str):
81-
return f"{open_indent}{value}" if depth else value
76+
return value
8277

8378
try:
8479
tag = value["tagName"]
8580
except TypeError as error: # pragma: no cover
8681
raise TypeError(f"Expected a VDOM dictionary or string, not {value}") from error
8782

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+
)
12386

124-
children = "".join(children_list)
87+
if attributes:
88+
assert tag, "Element frament may not contain attributes"
89+
attributes = f" {attributes}"
12590

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+
)
12897

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}/>")
136105
)
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
148108
)
149109

150110

@@ -210,6 +170,10 @@ def html_to_vdom(
210170
return vdom
211171

212172

173+
class HTMLParseError(etree.LxmlSyntaxError): # type: ignore[misc]
174+
"""Raised when an HTML document cannot be parsed using strict parsing."""
175+
176+
213177
def _etree_to_vdom(
214178
node: etree._Element, transforms: Iterable[_ModelTransform]
215179
) -> VdomDict:
@@ -313,5 +277,32 @@ def _hypen_to_camel_case(string: str) -> str:
313277
return first.lower() + remainder.title().replace("-", "")
314278

315279

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))}"'

tests/test_utils.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from html import escape as html_escape
2+
13
import pytest
24

35
import idom
@@ -152,6 +154,9 @@ def test_html_to_vdom_with_no_parent_node():
152154
assert html_to_vdom(source) == expected
153155

154156

157+
SOME_OBJECT = object()
158+
159+
155160
@pytest.mark.parametrize(
156161
"vdom_in, html_out",
157162
[
@@ -160,20 +165,20 @@ def test_html_to_vdom_with_no_parent_node():
160165
"<div>hello</div>",
161166
),
162167
(
163-
html.div(object()), # ignore non-str/vdom children
164-
"<div />",
168+
html.div(SOME_OBJECT),
169+
f"<div>{html_escape(str(SOME_OBJECT))}</div>",
165170
),
166171
(
167-
html.div({"someAttribute": object()}), # ignore non-str/vdom attrs
168-
"<div />",
172+
html.div({"someAttribute": SOME_OBJECT}),
173+
f'<div someattribute="{html_escape(str(SOME_OBJECT))}" />',
169174
),
170175
(
171176
html.div("hello", html.a({"href": "https://example.com"}, "example")),
172177
'<div>hello<a href="https://example.com">example</a></div>',
173178
),
174179
(
175180
html.button({"onClick": lambda event: None}),
176-
"<button />",
181+
"<button/>",
177182
),
178183
(
179184
html.div({"style": {"backgroundColor": "blue", "marginLeft": "10px"}}),
@@ -198,19 +203,9 @@ def test_html_to_vdom_with_no_parent_node():
198203
),
199204
html.button(),
200205
),
201-
'<div><div>hello</div><a href="https://example.com">example</a><button /></div>',
206+
'<div><div>hello</div><a href="https://example.com">example</a><button/></div>',
202207
),
203208
],
204209
)
205210
def test_vdom_to_html(vdom_in, html_out):
206211
assert vdom_to_html(vdom_in) == html_out
207-
208-
209-
def test_vdom_to_html_with_indent():
210-
assert (
211-
vdom_to_html(
212-
html.div("hello", html.a({"href": "https://example.com"}, "example")),
213-
indent=2,
214-
)
215-
== '<div>\n hello\n <a href="https://example.com">\n example\n </a>\n</div>'
216-
)

0 commit comments

Comments
 (0)