Skip to content

Commit

Permalink
Self closing children fix (#11)
Browse files Browse the repository at this point in the history
* fix

* clean up
  • Loading branch information
keithasaurus authored Dec 23, 2023
1 parent 5b148c3 commit 2975c13
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 29 deletions.
26 changes: 18 additions & 8 deletions simple_html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,29 @@ def escape_attribute_key(k: str) -> str:


class Tag:
__slots__ = ("tag_start", "rendered", "closing_tag", "no_children_close")
__slots__ = (
"tag_start",
"closing_tag",
"tag_start_no_attrs",
"rendered",
"no_children_close",
)

def __init__(self, name: str, self_closing: bool = False) -> None:
self.tag_start = f"<{name}"
self.tag_start_no_attrs = f"{self.tag_start}>"
self.closing_tag = f"</{name}>"
if self_closing:
self.closing_tag = ""
self.no_children_close = "/>"
else:
self.closing_tag = f"</{name}>"
self.no_children_close = f">{self.closing_tag}"
self.rendered = f"{self.tag_start}{self.no_children_close}"

def __call__(
self,
attributes: Dict[Union[SafeString, str], Union[str, SafeString, None]],
*children: Node,
) -> TagTuple:
) -> Union[TagTuple, SafeString]:
if attributes:
# in this case this is faster than attrs = "".join([...])
attrs = ""
Expand All @@ -126,6 +132,7 @@ def __call__(
if isinstance(key, SafeString)
else escape_attribute_key(key)
)

if isinstance(val, str):
attrs += f' {key}="{escape(val, True)}"'
elif isinstance(val, SafeString):
Expand All @@ -136,8 +143,11 @@ def __call__(
if children:
return f"{self.tag_start}{attrs}>", children, self.closing_tag
else:
return f"{self.tag_start}{attrs}{self.no_children_close}", children, ""
return f"{self.tag_start}>", children, self.closing_tag
return SafeString(f"{self.tag_start}{attrs}{self.no_children_close}")
elif children:
return self.tag_start_no_attrs, children, self.closing_tag
else:
return SafeString(self.rendered)


DOCTYPE_HTML5 = SafeString("<!doctype html>")
Expand Down Expand Up @@ -265,10 +275,10 @@ def _render(nodes: Iterable[Node], strs: List[str]) -> None:
strs.append(node[0])
_render(node[1], strs)
strs.append(node[2])
elif isinstance(node, str):
strs.append(escape(node))
elif isinstance(node, SafeString):
strs.append(node.safe_str)
elif isinstance(node, str):
strs.append(escape(node))
elif isinstance(node, Tag):
strs.append(node.rendered)
elif isinstance(node, list):
Expand Down
52 changes: 31 additions & 21 deletions tests/test_simple_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
Node,
DOCTYPE_HTML5,
render,
escape_attribute_key, render_styles,
escape_attribute_key,
render_styles,
img,
)


Expand Down Expand Up @@ -114,8 +116,8 @@ def test_kw_attributes() -> None:
node = div({"class": "first", "name": "some_name", "style": "color:blue;"}, "okok")

assert (
render(node)
== '<div class="first" name="some_name" style="color:blue;">okok</div>'
render(node)
== '<div class="first" name="some_name" style="color:blue;">okok</div>'
)


Expand All @@ -129,6 +131,11 @@ def test_attribute_without_value_rendered_as_expected() -> None:
assert render(a({"something": None})) == "<a something></a>"


def test_self_closing_still_nests_children() -> None:
assert render(img({}, div)) == "<img><div></div></img>"
assert render(img({"id": "4"}, div)) == '<img id="4"><div></div></img>'


def test_render_with_doctype() -> None:
assert render(DOCTYPE_HTML5, html) == "<!doctype html><html></html>"

Expand Down Expand Up @@ -156,8 +163,8 @@ def test_render_kw_attribute_with_none() -> None:
def test_can_render_empty() -> None:
assert render([]) == ""
assert (
render(div({}, [], "hello ", [], span({}, "World!"), []))
== "<div>hello <span>World!</span></div>"
render(div({}, [], "hello ", [], span({}, "World!"), []))
== "<div>hello <span>World!</span></div>"
)


Expand All @@ -175,33 +182,33 @@ def test_escape_key() -> None:
assert escape_attribute_key("=") == "&#x3D;"
assert escape_attribute_key("`") == "&#x60;"
assert (
escape_attribute_key("something with spaces")
== "something&nbsp;with&nbsp;spaces"
escape_attribute_key("something with spaces")
== "something&nbsp;with&nbsp;spaces"
)


def test_render_with_escaped_attributes() -> None:
assert (
render(div({'onmousenter="alert(1)" noop': "1"}))
== '<div onmousenter&#x3D;&quot;alert(1)&quot;&nbsp;noop="1"></div>'
render(div({'onmousenter="alert(1)" noop': "1"}))
== '<div onmousenter&#x3D;&quot;alert(1)&quot;&nbsp;noop="1"></div>'
)
assert (
render(span({"<script>\"</script>": "\">"}))
== '<span &lt;script&gt;&quot;&lt;/script&gt;="&quot;&gt;"></span>'
render(span({'<script>"</script>': '">'}))
== '<span &lt;script&gt;&quot;&lt;/script&gt;="&quot;&gt;"></span>'
)
# vals and keys escape slightly differently
assert (
render(div({'onmousenter="alert(1)" noop': 'onmousenter="alert(1)" noop'}))
== '<div onmousenter&#x3D;&quot;alert(1)&quot;&nbsp;noop="onmousenter=&quot;alert(1)&quot; noop"></div>'
render(div({'onmousenter="alert(1)" noop': 'onmousenter="alert(1)" noop'}))
== '<div onmousenter&#x3D;&quot;alert(1)&quot;&nbsp;noop="onmousenter=&quot;alert(1)&quot; noop"></div>'
)


def test_render_with_safestring_attributes() -> None:
bad_key = 'onmousenter="alert(1)" noop'
bad_val = "<script></script>"
assert (
render(div({SafeString(bad_key): SafeString(bad_val)}))
== f'<div {bad_key}="{bad_val}"></div>'
render(div({SafeString(bad_key): SafeString(bad_val)}))
== f'<div {bad_key}="{bad_val}"></div>'
)


Expand All @@ -218,14 +225,17 @@ def test_safe_string_eq() -> None:
def test_render_styles() -> None:
assert render_styles({}) == SafeString("")
assert render_styles({"abc": 123.45}) == SafeString("abc:123.45;")
assert render_styles({"padding": 0,
"margin": "0 10"}) == SafeString("padding:0;margin:0 10;")
assert render_styles({"padding": 0, "margin": "0 10"}) == SafeString(
"padding:0;margin:0 10;"
)

assert render(div({"style": render_styles({"min-width": "25px"})},
"cool")) == '<div style="min-width:25px;">cool</div>'
assert (
render(div({"style": render_styles({"min-width": "25px"})}, "cool"))
== '<div style="min-width:25px;">cool</div>'
)


def test_render_styles_escapes() -> None:
assert render_styles({'"><': "><>\""}) == SafeString(
safe_str='&quot;&gt;&lt;:&gt;&lt;&gt;&quot;;'
assert render_styles({'"><': '><>"'}) == SafeString(
safe_str="&quot;&gt;&lt;:&gt;&lt;&gt;&quot;;"
)

0 comments on commit 2975c13

Please sign in to comment.