From 3d69a9b5bde1263b49a47a2ff9a52f977b6a75ab Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Fri, 11 Oct 2024 18:18:59 +0200 Subject: [PATCH] Moved `html` module to core --- emmett/html.py | 206 ++++++--------------------------------------- pyproject.toml | 2 +- tests/test_html.py | 42 --------- 3 files changed, 26 insertions(+), 224 deletions(-) delete mode 100644 tests/test_html.py diff --git a/emmett/html.py b/emmett/html.py index 7beea68c..38b41a89 100644 --- a/emmett/html.py +++ b/emmett/html.py @@ -9,129 +9,35 @@ :license: BSD-3-Clause """ -import html import re -import threading from functools import reduce +from emmett_core.html import ( + MetaHtmlTag as _MetaHtmlTag, + TagStack, + TreeHtmlTag, + _to_str, + cat as cat, + htmlescape as htmlescape, +) -__all__ = ["tag", "cat", "asis"] - - -class TagStack(threading.local): - def __init__(self): - self.stack = [] - - def __getitem__(self, key): - return self.stack[key] - - def append(self, item): - self.stack.append(item) - - def pop(self, idx): - self.stack.pop(idx) - - def __bool__(self): - return len(self.stack) > 0 - - -class HtmlTag: - __slots__ = ["name", "parent", "components", "attributes"] - - rules = { - "ul": ["li"], - "ol": ["li"], - "table": ["tr", "thead", "tbody"], - "thead": ["tr"], - "tbody": ["tr"], - "tr": ["td", "th"], - "select": ["option", "optgroup"], - "optgroup": ["optionp"], - } - _self_closed = {"br", "col", "embed", "hr", "img", "input", "link", "meta"} - def __init__(self, name): - self.name = name - self.parent = None - self.components = [] - self.attributes = {} - if _stack: - _stack[-1].append(self) +__all__ = ["tag", "cat", "asis"] - def __enter__(self): - _stack.append(self) - return self +_re_tag = re.compile(r"^([\w\-\:]+)") +_re_id = re.compile(r"#([\w\-]+)") +_re_class = re.compile(r"\.([\w\-]+)") +_re_attr = re.compile(r"\[([\w\-\:]+)=(.*?)\]") - def __exit__(self, type, value, traceback): - _stack.pop(-1) - @staticmethod - def wrap(component, rules): - if rules and (not isinstance(component, HtmlTag) or component.name not in rules): - return HtmlTag(rules[0])(component) - return component +class HtmlTag(TreeHtmlTag): + __slots__ = [] 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): - component.parent = self - return self - - def append(self, component): - self.components.append(component) - - def insert(self, i, component): - self.components.insert(i, component) - - def remove(self, component): - self.components.remove(component) - - def __getitem__(self, key): - if isinstance(key, int): - return self.components[key] - else: - return self.attributes.get(key) - - def __setitem__(self, key, value): - if isinstance(key, int): - self.components.insert(key, value) - else: - self.attributes[key] = value - - def __iter__(self): - for item in self.components: - yield item - - def __str__(self): - return self.__html__() - - def __add__(self, other): - return cat(self, other) - - def add_class(self, name): - """add a class to _class attribute""" - c = self["_class"] - classes = (set(c.split()) if c else set()) | set(name.split()) - self["_class"] = " ".join(classes) if classes else None - return self - - def remove_class(self, name): - """remove a class from _class attribute""" - c = self["_class"] - classes = (set(c.split()) if c else set()) - set(name.split()) - self["_class"] = " ".join(classes) if classes else None - return self - - regex_tag = re.compile(r"^([\w\-\:]+)") - regex_id = re.compile(r"#([\w\-]+)") - regex_class = re.compile(r"\.([\w\-]+)") - regex_attr = re.compile(r"\[([\w\-\:]+)=(.*?)\]") + return super().__call__(*components, **attributes) def find(self, expr): union = lambda a, b: a.union(b) @@ -141,15 +47,15 @@ def find(self, expr): tags = [self] for k, item in enumerate(expr.split()): if k > 0: - children = [{c for c in tag if isinstance(c, HtmlTag)} for tag in tags] + children = [{c for c in tag if isinstance(c, self.__class__)} for tag in tags] tags = reduce(union, children) tags = reduce(union, [tag.find(item) for tag in tags], set()) else: - tags = reduce(union, [c.find(expr) for c in self if isinstance(c, HtmlTag)], set()) - tag = HtmlTag.regex_tag.match(expr) - id = HtmlTag.regex_id.match(expr) - _class = HtmlTag.regex_class.match(expr) - attr = HtmlTag.regex_attr.match(expr) + tags = reduce(union, [c.find(expr) for c in self if isinstance(c, self.__class__)], set()) + tag = _re_tag.match(expr) + id = _re_id.match(expr) + _class = _re_class.match(expr) + attr = _re_attr.match(expr) if ( (tag is None or self.name == tag.group(1)) and (id is None or self["_id"] == id.group(1)) @@ -159,59 +65,10 @@ 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(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() - attrs = " " + attrs if attrs else "" - if name in self._self_closed: - return "<%s%s />" % (name, attrs) - components = "".join(htmlescape(v) for v in self.components) - return "<%s%s>%s" % (name, attrs, components, name) - - def __json__(self): - return str(self) - -class MetaHtmlTag: - def __getattr__(self, name): - return HtmlTag(name) - - def __getitem__(self, name): - return HtmlTag(name) - - -class cat(HtmlTag): +class MetaHtmlTag(_MetaHtmlTag): __slots__ = [] - - def __init__(self, *components): - self.components = list(components) - self.attributes = {} - - def __html__(self): - return "".join(htmlescape(v) for v in self.components) + _tag_cls = HtmlTag class asis(HtmlTag): @@ -224,17 +81,4 @@ def __html__(self): return _to_str(self.name) -def _to_str(obj): - if not isinstance(obj, str): - return str(obj) - return obj - - -def htmlescape(obj): - if hasattr(obj, "__html__"): - return obj.__html__() - return html.escape(_to_str(obj), True).replace("'", "'") - - -_stack = TagStack() -tag = MetaHtmlTag() +tag = MetaHtmlTag(TagStack()) diff --git a/pyproject.toml b/pyproject.toml index 504776e7..7408e819 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,4 +149,4 @@ dev-dependencies = [ ] [tool.uv.sources] -emmett-core = { git = "https://github.com/emmett-framework/core", rev = "a0f8ec8" } +emmett-core = { git = "https://github.com/emmett-framework/core", rev = "aa7b303" } diff --git a/tests/test_html.py b/tests/test_html.py deleted file mode 100644 index 25a7153c..00000000 --- a/tests/test_html.py +++ /dev/null @@ -1,42 +0,0 @@ -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"}'