from copy import copy from dataclasses import dataclass from enum import StrEnum from typing import TYPE_CHECKING, Iterable, Optional, Self from bs4 import BeautifulSoup, Tag from ..clonable import Clonable if TYPE_CHECKING: from ..puppytype import Parsable TemplateName = "template" class ShadowRootMode(StrEnum): Open = "open" Closed = "closed" class Node(Clonable): @property def value(self): pass def from_path(path: str) -> Self: with open(path, "r") as f: soup = BeautifulSoup(f.read(), 'html.parser') return Node.from_tag(soup) def from_tag(tag: Tag | Iterable[Tag] | BeautifulSoup) -> Self: if isinstance(tag, Tag): return SingleNode(tag) match len(tag): case 0: return EmptyNode() case 1: return SingleNode(tag[0]) case _: return NodeCollection(tag) def attach_shadow(self, shadow_root_mode: ShadowRootMode, clonable: Optional[bool] = None, delegates_focus: Optional[bool] = None, serializable: Optional[bool] = None): return ShadowRoot( host=self.value, mode=shadow_root_mode, clonable=clonable, delegates_focus=delegates_focus, serializable=serializable ) def find(self, *args, **kwargs): return self.value.find(*args, **kwargs) def find_all(self, *args, **kwargs): return self.value.find_all(*args, **kwargs) def append(self, *args, **kwargs): return self.value.append(*args, **kwargs) def insert(self, *args, **kwargs): return self.value.insert(*args, **kwargs) def get_attr(self, attr: str) -> Optional[str]: return self.value.get(attr) def has_attr(self, attr: str) -> bool: return self.value.has_attr(attr) def replace(self, node: "Node | Iterable[Node]"): self.value.insert_before(node.value) self.value.decompose() def prettify(self) -> str: return self.value.prettify() class EmptyNode(Node): @property def value(self): return None @dataclass class NodeCollection(Node): nodes: Iterable[Tag] def __iter__(self): return self.nodes @property def value(self): return self.nodes def clone(self) -> Self: return NodeCollection(copy(self.value)) @dataclass class SingleNode(Node): _value: Tag @property def value(self): return self._value @property def parent(self): return self._value.parent def attach_shadow(self, shadow_root_mode: ShadowRootMode, clonable: Optional[bool] = None, delegates_focus: Optional[bool] = None, serializable: Optional[bool] = None): return ShadowRoot( host=self, mode=shadow_root_mode, clonable=clonable, delegates_focus=delegates_focus, serializable=serializable ) def __getitem__(self, index): return self._value[index] def clone(self) -> Self: return SingleNode(copy(self.value)) @dataclass class Template(Node): _value: Node @property def value(self): return self._value @property def id(self): return self._value["id"] @property def shadow_root_mode(self) -> ShadowRootMode | None: return self._value.get("shadowrootmode") @shadow_root_mode.setter def shadow_root_mode(self, mode: ShadowRootMode): self._value["shadowrootmode"] = mode.value @property def delegates_focus(self) -> Optional[bool]: return self._value.get("delegatesfocus") @delegates_focus.setter def delegates_focus(self, value: bool): self._value["delegatesfocus"] = value @property def clonable(self) -> Optional[bool]: return self._value.get("clonable") @clonable.setter def clonable(self, value: bool): self._value["clonable"] = value @property def serializable(self) -> Optional[bool]: return self._value.get("serializable") @serializable.setter def serializable(self, value: bool): self._value["serializable"] = value def from_path(path: str) -> Self: with open(path, "r") as f: return Template(BeautifulSoup(f.read(), 'html.parser').find()) def from_element(element: "Parsable") -> Self: if isinstance(element, Template): return element return Template(element) def clone(self) -> Self: return Template(copy(self._value)) @dataclass class ShadowRoot(Clonable): host: Node mode: ShadowRootMode clonable: Optional[bool] = None delegates_focus: Optional[bool] = None serializable: Optional[bool] = None def append_child(self, template: Template): template.shadow_root_mode = self.mode if self.clonable is not None: template.clonable = self.clonable if self.delegates_focus is not None: template.delegates_focus = self.delegates_focus if self.serializable is not None: template.serializable = self.serializable self.host.value.insert(0, template.value)