117 lines
2.8 KiB
Python
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))
|