from copy import copy from dataclasses import InitVar, dataclass, field from typing import TYPE_CHECKING, Self from bs4 import BeautifulSoup, Tag from protocols import Clonable, Renderable if TYPE_CHECKING: from puppygirl.puppygirl import Puppygirl from puppygirl.puppytype import ElementLike PgSlotName = "puppygirl-slot" TemplateName = "template" SlotName = "slot" IdAttr = "id" NameAttr = "name" TemplateAttr = "template" UnnamedSlotId = "unnamed" @dataclass class PuppygirlElement(Renderable): puppygirl: "Puppygirl" @dataclass class Element(Clonable): value: InitVar["ElementLike"] def __post_init__(self, value: "ElementLike"): if isinstance(value, str): value = BeautifulSoup(value, features = "html.parser") if isinstance(value, BeautifulSoup): value = next(iter(value)) self.value = value def from_element_like(value: "ElementLike") -> Self: if isinstance(value, Element): return value return Element(value) def clone(self) -> Self: return Element(copy(self.value)) # proxy all calls to inner template def __getattr__(self, name): return getattr(self.value, name) def __getitem__(self, index): return self.value[index] class TemplateSlot(Element): is_default: bool = True def append(self, value): if self.is_default: self.is_default = False self.value.clear() self.value.append(value) @dataclass class TemplateInstance(Element, Clonable): slots: dict[str, Tag] = field(init = False) slot_instances: dict[str, TemplateSlot] = field(default_factory=dict) def __post_init__(self, value: "ElementLike"): Element.__post_init__(self, value) slots = {} for slot in value.find_all(SlotName): if slot.has_attr(NameAttr): slots[slot[NameAttr]] = slot else: slots[UnnamedSlotId] = slot self.slots = slots def _get_slot(self, slot_name: str) -> TemplateSlot: slot = self.slot_instances.get(slot_name) if slot is not None: return slot slot = TemplateSlot(self.slots.get(slot_name)) self.slot_instances[slot_name] = slot return slot def insert_content(self, content: "ElementLike"): slot_name = content.get(SlotName) if slot_name is None: slot_name = UnnamedSlotId slot = self._get_slot(slot_name) if slot is not None: slot.append(content) def remove_slots(self): for slot in self.slots.values(): slot.unwrap() @dataclass class Template(Element, Clonable[TemplateInstance]): def __post_init__(self, value: "ElementLike"): Element.__post_init__(self, value) if value.name != TemplateName: raise TypeError(f"{value} is not a template") if not value.has_attr(IdAttr): raise TypeError(f"{value} missing id attribute") def clone(self) -> TemplateInstance: return TemplateInstance(copy(self.value))