Skip to content

Commit

Permalink
Moved html module to core
Browse files Browse the repository at this point in the history
  • Loading branch information
gi0baro committed Oct 11, 2024
1 parent 85b2da8 commit 3d69a9b
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 224 deletions.
206 changes: 25 additions & 181 deletions emmett/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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</%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):
Expand All @@ -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("'", "&#39;")


_stack = TagStack()
tag = MetaHtmlTag()
tag = MetaHtmlTag(TagStack())
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
42 changes: 0 additions & 42 deletions tests/test_html.py

This file was deleted.

0 comments on commit 3d69a9b

Please sign in to comment.