Source code for domilite.dom_tag

import contextlib
import dataclasses as dc
import sys
from collections.abc import Iterator
from enum import auto
from typing import TYPE_CHECKING
from typing import ClassVar
from typing import Self
from typing import overload

from markupsafe import Markup

from .accessors import AttributesProperty
from .flags import Flag
from .render import RenderFlags
from .render import RenderStream

__all__ = ["dom_tag", "Flags"]

if not TYPE_CHECKING and sys.version_info < (3, 5, 2):  # pragma: no cover

    def overload(f):
        return f


[docs] class Flags(Flag): """ Set rendering properties of individual tags """ #: This tag is self-closing SINGLE = auto() #: This tag should have formatted content rendered with indentation PRETTY = auto() #: When possible, this tag should be shown inline with thte parent tag. INLINE = auto()
def _trace_live(msg: str) -> None: import inspect import logging logger = logging.getLogger() stack = inspect.stack() frame = stack[1] self = frame.frame.f_locals.get("self", None) name = getattr(self, "name", "unknown") logger.debug(f"[<{name}>:{frame.filename}:{frame.lineno} in {frame.function}] {msg}") def _trace_noop(msg: str) -> None: # pragma: no cover pass _trace = _trace_noop @contextlib.contextmanager def render_tracing() -> Iterator[None]: """This is a helper function to show log messages when rendering elements """ global _trace try: _trace = _trace_live yield finally: _trace = _trace_noop def normalize_name(name: str) -> str: if name.startswith("_"): name = name.removeprefix("_") if name.endswith("_"): name = name.removesuffix("_") return name @dc.dataclass(slots=True) class Name: """pseudo-classproperty for accessing the .name attribute on both the type and instance.""" def __get__(self, instance: "dom_tag | None", owner: type["dom_tag"]) -> str: name = getattr(owner, "__tagname__", owner.__name__) return normalize_name(name)
[docs] class dom_tag: """ Base class for any tag object. Subclass this to create custom tags. Parameters ---------- `*args`: string or tag Provide child tags, which will be added via :meth:`add` `**kwargs`: string or bool Provide attribute values, which will be added to :attr:`attributes` """ __slots__ = ("_attributes_inner", "children", "__weakref__") #: Rendering flags for this tag. flags: ClassVar["Flags"] = Flags.PRETTY #: The attributes associated with this tag. See :class:`~domilite.accessors.Attributes` for more details. attributes: ClassVar[AttributesProperty["dom_tag"]] = AttributesProperty() #: The (HTML) classes associated with this tag, managed as a set of strings. See :class:`~domilite.accessors.Classes` for more details. classes = attributes.classes() #: The list of child tags or markup objects children: list["dom_tag | Markup"] #: The name of this tag, inferred from the name of the class name: Name = Name() def __init__(self, *args: "str | dom_tag | Markup", **kwargs: str | bool) -> None: self.attributes.update(kwargs) self.children = [] self.add(*args) def __eq__(self, other: object) -> bool: if not isinstance(other, dom_tag): return NotImplemented return (self.name == other.name) and (self.attributes == other.attributes) and (self.children == other.children)
[docs] @classmethod def find_tag_type(cls, name: str) -> type[Self] | None: """Find a particular subclass of this tag with a given name.""" normalized = normalize_name(name) for scls in cls.iter_subclasses(): if scls.name == normalized: return scls return None
[docs] @classmethod def iter_subclasses(cls) -> Iterator[type[Self]]: """Iterate through all known subclasses of this tag, recursively.""" yield cls for scls in cls.__subclasses__(): yield scls yield from scls.iter_subclasses()
[docs] def add(self, *children: "dom_tag | str | Markup") -> Self: """Add child tags to this tag. Parameters ---------- children: tag, string, or Markup Tags become children, strings are escaped, and Markup is text that is added raw to this tag. Returns ------- self: this tag, to facilitate method chaining """ for child in children: if isinstance(child, str): child = Markup.escape(child) self.children.append(child) return self
[docs] def remove(self, child: "dom_tag | Markup") -> Self: """ Remove a particular child. If you are removing a string, escape it first with :meth:`Markup.escape`. Parameters ---------- child: tag or Markup Child to remove. If it doesn't exist, an error will be raised. Returns ------- self: this tag, to facilitate method chaining """ self.children.remove(child) return self
[docs] def clear(self) -> Self: """Remove all chilren. Returns ------- self: this tag, to facilitate method chaining """ self.children.clear() return self
@overload def __getitem__(self, index: int) -> "dom_tag | Markup": ... @overload def __getitem__(self, index: str) -> "str | bool": ... # noqa: F811 def __getitem__(self, index: int | str) -> "dom_tag | Markup | str | bool": # noqa: F811 if isinstance(index, int): try: return self.children[index] except IndexError: raise IndexError(f"Index for children out of range: {index}") elif isinstance(index, str): try: return self.attributes[index] except KeyError: raise KeyError(f"Attribute not found: {index}") else: raise TypeError(f"Invalid index type: {type(index)}") @overload def __setitem__(self, index: int, value: "str |dom_tag | Markup") -> None: ... @overload def __setitem__(self, index: str, value: "str | bool") -> None: ... # noqa: F811 def __setitem__( # noqa: F811 self, index: int | str, value: "str | bool | dom_tag | Markup" ) -> None: if isinstance(index, int): if isinstance(value, str): value = Markup.escape(value) elif isinstance(value, bool): raise TypeError(f"Invalid child type: {type(value)}") try: self.children[index] = value except IndexError: raise IndexError(f"Index for children out of range: {index}") elif isinstance(index, str): if not isinstance(value, (str, bool)): raise TypeError(f"Invalid value type for attribute: {type(value)}") self.attributes[index] = value else: raise TypeError(f"Invalid index type: {type(index)}") def __delitem__(self, index: str | int) -> None: if isinstance(index, int): try: del self.children[index] except IndexError: raise IndexError(f"Index for children out of range: {index}") elif isinstance(index, str): try: del self.attributes[index] except KeyError: raise KeyError(f"Attribute not found: {index}") else: raise TypeError(f"Invalid index type: {type(index)}") def __iter__(self) -> Iterator["dom_tag | Markup"]: return iter(self.children) def __len__(self) -> int: return len(self.children) def __bool__(self) -> bool: return True __nonzero__ = __bool__
[docs] def render( self, indent: str = " ", flags: RenderFlags = RenderFlags.PRETTY, pretty: bool | None = None, xhtml: bool | None = None, ) -> str: """Render this tree of tags to a string. Parameters ---------- indent: str, optional String to use for indenting in `pretty` mode. Defaults to two spaces: ` ` flags: :class:`~domilite.render.RenderFlags` Adjust the rendering properties to use (e.g. turn off PRETTY) pretty: bool or None Explicitly enable or disable pretty rendering. xhtml: bool or None Explicitly enable or disable xhtml rendering. """ flags = flags.with_arguments(pretty=pretty, xhtml=xhtml) stream = RenderStream(indent, flags) _trace(f"_render {flags}") self._render(stream) return stream.getvalue()
def __str__(self) -> str: return self.render() def __html__(self) -> str: return self.render() def _render(self, stream: RenderStream) -> None: pretty = stream.flags.is_pretty and self.flags.is_pretty _trace("open <") stream.write("<") stream.write(self.name) if self.attributes: _trace(f"attributes {len(self.attributes)}") stream.write(" ") self.attributes._render(stream) if (self.flags & Flags.SINGLE) and (stream.flags & RenderFlags.XHTML): _trace("open single xhtml />") stream.write(" />") else: _trace("open tag >") stream.write(">") if self.flags & Flags.SINGLE: return with stream.indented(): _trace(f"children: {len(self.children)}") inline = self._render_children(stream) if pretty and not inline: stream.newline() _trace("close tag </ >") stream.write(f"</{self.name}>") return def _render_children(self, stream: RenderStream) -> bool: inline = True for child in self.children: if isinstance(child, dom_tag): if (RenderFlags.PRETTY in stream.flags) and Flags.INLINE not in child.flags: _trace("newline()") inline = False stream.newline() _trace(f"_render {child.name}") child._render(stream) elif isinstance(child, Markup): _trace("write") stream.write(child) else: raise TypeError(f"Unsupported child type: {type(child)}") return inline def __repr__(self) -> str: parts = [f"{type(self).__module__}.{self.name}"] if self.attributes: if len(self.attributes) == 1: parts.append("1 attribute") else: parts.append(f"{len(self.attributes)} attributes") if self.children: if len(self.children) == 1: parts.append("1 child") else: parts.append(f"{len(self.children)} children") return "<" + " ".join(parts) + ">"
[docs] def descendants(self) -> Iterator["dom_tag"]: """Iterate over all children and children of children recursively.""" for child in self.children: if isinstance(child, dom_tag): yield child yield from child.descendants()