puppygirl-py/puppygirl/elements/__init__.py
2025-10-06 06:31:11 -04:00

117 lines
2.8 KiB
Python

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))