From e2b0ab0fc099fd03e933188760bccf27aa144e81 Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Thu, 10 Oct 2024 23:42:14 +0200 Subject: [PATCH] Review `html` module --- emmett/html.py | 45 +++++++++++++++++++++++++++++++++------------ tests/test_html.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 tests/test_html.py diff --git a/emmett/html.py b/emmett/html.py index 193f6c44..7beea68c 100644 --- a/emmett/html.py +++ b/emmett/html.py @@ -36,6 +36,8 @@ def __bool__(self): class HtmlTag: + __slots__ = ["name", "parent", "components", "attributes"] + rules = { "ul": ["li"], "ol": ["li"], @@ -72,6 +74,9 @@ def wrap(component, rules): def __call__(self, *components, **attributes): rules = self.rules.get(self.name, []) self.components = [self.wrap(comp, rules) for comp in components] + # legacy "data" attribute + if _data := attributes.pop("data", None): + attributes["_data"] = _data self.attributes = attributes for component in self.components: if isinstance(component, HtmlTag): @@ -154,20 +159,32 @@ def find(self, expr): tags.add(self) return tags + @staticmethod + def _build_html_attributes_items(attrs, namespace=None): + if namespace: + for k, v in sorted(attrs.items()): + nk = f"{namespace}-{k}" + if v is True: + yield (nk, k) + else: + yield (nk, htmlescape(v)) + else: + for k, v in filter(lambda item: item[0].startswith("_") and item[1] is not None, sorted(attrs.items())): + nk = k[1:] + if isinstance(v, dict): + for item in HtmlTag._build_html_attributes_items(v, nk): + yield item + elif v is True: + yield (nk, nk) + else: + yield (nk, htmlescape(v)) + def _build_html_attributes(self): - return " ".join( - '%s="%s"' % (k[1:], k[1:] if v is True else htmlescape(v)) - for (k, v) in sorted(self.attributes.items()) - if k.startswith("_") and v is not None - ) + return " ".join(f'{k}="{v}"' for k, v in self._build_html_attributes_items(self.attributes)) def __html__(self): name = self.name attrs = self._build_html_attributes() - data = self.attributes.get("data", {}) - data_attrs = " ".join('data-%s="%s"' % (k, htmlescape(v)) for k, v in data.items()) - if data_attrs: - attrs = attrs + " " + data_attrs attrs = " " + attrs if attrs else "" if name in self._self_closed: return "<%s%s />" % (name, attrs) @@ -187,6 +204,8 @@ def __getitem__(self, name): class cat(HtmlTag): + __slots__ = [] + def __init__(self, *components): self.components = list(components) self.attributes = {} @@ -196,11 +215,13 @@ def __html__(self): class asis(HtmlTag): - def __init__(self, text): - self.text = text + __slots__ = [] + + def __init__(self, val): + self.name = val def __html__(self): - return _to_str(self.text) + return _to_str(self.name) def _to_str(obj): diff --git a/tests/test_html.py b/tests/test_html.py new file mode 100644 index 00000000..25a7153c --- /dev/null +++ b/tests/test_html.py @@ -0,0 +1,42 @@ +from emmett.html import asis, cat, tag + + +def test_tag_self_closed(): + br = tag.br() + assert str(br) == "
" + + +def test_tag_non_closed(): + p = tag.p() + assert str(p) == "

" + + +def test_tag_components(): + t = tag.div(tag.p(), tag.p()) + assert str(t) == "

" + + +def test_tag_attributes(): + d = tag.div(_class="test", _id="test", _test="test") + assert str(d) == '
' + + +def test_tag_attributes_dict(): + d = tag.div(_class="test", _hx={"foo": "bar"}) + assert str(d) == '
' + + +def test_tag_attributes_data(): + d1 = tag.div(data={"foo": "bar"}) + d2 = tag.div(_data={"foo": "bar"}) + assert str(d1) == str(d2) == '
' + + +def test_cat(): + t = cat(tag.p(), tag.p()) + assert str(t) == "

" + + +def test_asis(): + t = asis('{foo: "bar"}') + assert str(t) == '{foo: "bar"}'