uhh yeag
This commit is contained in:
parent
e4e49d265b
commit
698ed22f72
19 changed files with 291 additions and 196 deletions
4
puppygirl.egg-info/PKG-INFO
Normal file
4
puppygirl.egg-info/PKG-INFO
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: puppygirl
|
||||||
|
Version: 0.0.0
|
||||||
|
Requires-Dist: beautifulsoup4~=4.14.2
|
16
puppygirl.egg-info/SOURCES.txt
Normal file
16
puppygirl.egg-info/SOURCES.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
pyproject.toml
|
||||||
|
puppygirl/__init__.py
|
||||||
|
puppygirl/__main__.py
|
||||||
|
puppygirl/protocols.py
|
||||||
|
puppygirl/puppygirl.py
|
||||||
|
puppygirl/puppytype.py
|
||||||
|
puppygirl/renderer.py
|
||||||
|
puppygirl.egg-info/PKG-INFO
|
||||||
|
puppygirl.egg-info/SOURCES.txt
|
||||||
|
puppygirl.egg-info/dependency_links.txt
|
||||||
|
puppygirl.egg-info/requires.txt
|
||||||
|
puppygirl.egg-info/top_level.txt
|
||||||
|
puppygirl/elements/__init__.py
|
||||||
|
puppygirl/elements/constants.py
|
||||||
|
puppygirl/elements/domme.py
|
||||||
|
puppygirl/elements/shadow_root.py
|
1
puppygirl.egg-info/dependency_links.txt
Normal file
1
puppygirl.egg-info/dependency_links.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
1
puppygirl.egg-info/requires.txt
Normal file
1
puppygirl.egg-info/requires.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
beautifulsoup4~=4.14.2
|
1
puppygirl.egg-info/top_level.txt
Normal file
1
puppygirl.egg-info/top_level.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
puppygirl
|
|
@ -0,0 +1,3 @@
|
||||||
|
from puppygirl.clonable import *
|
||||||
|
from puppygirl.pg import *
|
||||||
|
from puppygirl.elements import *
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from puppygirl import Puppygirl
|
from puppygirl import Puppygirl
|
||||||
from elements.tag import PuppygirlTag
|
from elements.domme import PuppygirlDomme
|
||||||
|
from puppygirl.renderer import ClientSideRenderer, ServerSideRenderer
|
||||||
|
|
||||||
parser = ArgumentParser("puppygirl", add_help=True)
|
parser = ArgumentParser("puppygirl", add_help=True)
|
||||||
subparsers = parser.add_subparsers(dest="command")
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
@ -14,13 +15,15 @@ build.add_argument("-p", "--pretty", action="store_true")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
print(vars(args))
|
|
||||||
pg = Puppygirl([PuppygirlTag])
|
pg = Puppygirl(renderer=ClientSideRenderer(), elements=[PuppygirlDomme])
|
||||||
html = pg.fetch(args.input)
|
html = pg.fetch(args.input)
|
||||||
|
|
||||||
if args.pretty:
|
if args.pretty:
|
||||||
html = html.prettify()
|
html = html.prettify()
|
||||||
else:
|
else:
|
||||||
html = str(html)
|
html = str(html)
|
||||||
|
|
||||||
with open(args.output, "w") as f:
|
with open(args.output, "w") as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
|
|
||||||
|
|
17
puppygirl/assets/clientside.js
Normal file
17
puppygirl/assets/clientside.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class PuppygirlDomme extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
const templateName = this.getAttribute('template')
|
||||||
|
const template = document.getElementById(templateName)
|
||||||
|
|
||||||
|
const content = template.content
|
||||||
|
const shadow = this.attachShadow({ mode: 'open' })
|
||||||
|
shadow.appendChild(content.cloneNode(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
customElements.define("puppygirl-domme", PuppygirlDomme)
|
7
puppygirl/clonable.py
Normal file
7
puppygirl/clonable.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from copy import copy
|
||||||
|
from typing import TYPE_CHECKING, Protocol, Self
|
||||||
|
|
||||||
|
class Clonable[T = Self](Protocol):
|
||||||
|
def clone(self) -> T:
|
||||||
|
return copy(self)
|
||||||
|
|
|
@ -1,117 +1,3 @@
|
||||||
from copy import copy
|
from puppygirl.elements.constants import *
|
||||||
from dataclasses import InitVar, dataclass, field
|
from puppygirl.elements.domme import *
|
||||||
from typing import TYPE_CHECKING, Self
|
from puppygirl.elements.shadow_root import *
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
3
puppygirl/elements/constants.py
Normal file
3
puppygirl/elements/constants.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
IdAttr = "id"
|
||||||
|
TemplateAttr = "template"
|
||||||
|
TemplateName = "template"
|
31
puppygirl/elements/domme.py
Normal file
31
puppygirl/elements/domme.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from puppygirl.elements import TemplateAttr
|
||||||
|
from puppygirl.elements.shadow_root import ShadowRootMode
|
||||||
|
from puppygirl.renderer import Renderable
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from puppygirl import Puppygirl
|
||||||
|
from puppygirl.elements import Node
|
||||||
|
from puppygirl.puppytype import RenderableElement, Templates
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PuppygirlDomme(Renderable):
|
||||||
|
puppygirl: "Puppygirl"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "puppygirl-domme"
|
||||||
|
|
||||||
|
def render(self, node: "Node", templates: "Templates") -> "RenderableElement":
|
||||||
|
if not node.has_attr(TemplateAttr):
|
||||||
|
return node
|
||||||
|
|
||||||
|
template = templates[node[TemplateAttr]]
|
||||||
|
|
||||||
|
shadow_root = node.attach_shadow(ShadowRootMode.Open)
|
||||||
|
shadow_root.append_child(template.clone())
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
118
puppygirl/elements/shadow_root.py
Normal file
118
puppygirl/elements/shadow_root.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
from copy import copy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Optional, Self
|
||||||
|
|
||||||
|
from bs4 import Tag
|
||||||
|
from puppygirl.clonable import Clonable
|
||||||
|
|
||||||
|
TemplateName = "template"
|
||||||
|
|
||||||
|
class ShadowRootMode(StrEnum):
|
||||||
|
Open = "open"
|
||||||
|
Closed = "closed"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Node(Clonable):
|
||||||
|
_value: Tag
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
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 clone(self) -> "Self":
|
||||||
|
return Node(copy(self.value))
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name == '_value':
|
||||||
|
raise AttributeError()
|
||||||
|
return getattr(self._value, name)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._value[index]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Template(Clonable):
|
||||||
|
_value: Node
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@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 clone(self) -> Self:
|
||||||
|
return Template(copy(self._value))
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name == '_value':
|
||||||
|
raise AttributeError()
|
||||||
|
return getattr(self._value, name)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self._value[index]
|
||||||
|
|
||||||
|
def __setitem__(self, index, value):
|
||||||
|
self._value[index] = 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)
|
|
@ -1,30 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Iterable
|
|
||||||
from elements import PuppygirlElement, SlotName, TemplateAttr
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from elements import Element, TemplateInstance
|
|
||||||
from protocols import Clonable
|
|
||||||
from puppytype import RenderableElement, TemplateDict
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PuppygirlTag(PuppygirlElement):
|
|
||||||
name: str = "puppygirl-tag"
|
|
||||||
|
|
||||||
def apply_template(self, element: "Element", template: "Clonable[TemplateInstance]") -> Iterable["Element"]:
|
|
||||||
instance = template.clone()
|
|
||||||
|
|
||||||
for content in element.find_all(recursive=False):
|
|
||||||
instance.insert_content(content)
|
|
||||||
del content[SlotName]
|
|
||||||
|
|
||||||
instance.remove_slots()
|
|
||||||
return instance.value
|
|
||||||
|
|
||||||
def render(self, element: "Element", templates: "TemplateDict") -> "RenderableElement":
|
|
||||||
if not element.has_attr(TemplateAttr):
|
|
||||||
return element
|
|
||||||
|
|
||||||
template = templates.get(element[TemplateAttr])
|
|
||||||
return self.apply_template(element, template)
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
from typing import Callable, Iterable, Self
|
from typing import Callable, Iterable, Self
|
||||||
from bs4 import BeautifulSoup, Tag
|
from bs4 import BeautifulSoup, Tag
|
||||||
|
|
||||||
from elements import IdAttr, Renderable, Template, TemplateName
|
from elements import IdAttr, Template
|
||||||
from puppytype import ElementLike, ElementLikeList, Parsable, TemplateDict
|
from puppygirl.renderer import Renderable, Renderer
|
||||||
|
from puppygirl.puppytype import ElementLike, ElementLikeList, Parsable, Templates
|
||||||
|
|
||||||
class Puppygirl:
|
class Puppygirl[R: Renderer]:
|
||||||
|
renderer: R
|
||||||
elements: list[Renderable]
|
elements: list[Renderable]
|
||||||
templates: TemplateDict
|
templates: Templates
|
||||||
|
|
||||||
def __init__(self, elements: list[Callable[[Self], Renderable]] = [], templates: ElementLikeList = []):
|
def __init__(self, renderer: R, elements: list[Callable[[Self], Renderable]] = [], templates: ElementLikeList = []):
|
||||||
|
self.renderer = renderer
|
||||||
self.templates = Puppygirl._create_template_dict(templates)
|
self.templates = Puppygirl._create_template_dict(templates)
|
||||||
self.elements = [self._instantiate(el) for el in elements]
|
self.elements = [self._instantiate(el) for el in elements]
|
||||||
|
|
||||||
|
@ -17,7 +20,7 @@ class Puppygirl:
|
||||||
return value(self)
|
return value(self)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _create_template_dict(templates: Iterable[ElementLike]) -> TemplateDict:
|
def _create_template_dict(templates: Iterable[ElementLike]) -> Templates:
|
||||||
templates = [Template(t) for t in templates]
|
templates = [Template(t) for t in templates]
|
||||||
return {t[IdAttr]: t for t in templates}
|
return {t[IdAttr]: t for t in templates}
|
||||||
|
|
||||||
|
@ -25,11 +28,6 @@ class Puppygirl:
|
||||||
template = Template(template)
|
template = Template(template)
|
||||||
self._templates[template[IdAttr]] = template
|
self._templates[template[IdAttr]] = template
|
||||||
|
|
||||||
def _find_local_templates(tree: BeautifulSoup) -> TemplateDict:
|
|
||||||
templates = tree.find_all(TemplateName)
|
|
||||||
templates = filter(lambda t: t.has_attr(IdAttr), templates)
|
|
||||||
return Puppygirl._create_template_dict(templates)
|
|
||||||
|
|
||||||
def fetch(self, path: str) -> BeautifulSoup:
|
def fetch(self, path: str) -> BeautifulSoup:
|
||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
return self.parse(f)
|
return self.parse(f)
|
||||||
|
@ -41,16 +39,4 @@ class Puppygirl:
|
||||||
return self.parse_tree(BeautifulSoup(value, features='html.parser'))
|
return self.parse_tree(BeautifulSoup(value, features='html.parser'))
|
||||||
|
|
||||||
def parse_tree(self, tree: BeautifulSoup) -> BeautifulSoup:
|
def parse_tree(self, tree: BeautifulSoup) -> BeautifulSoup:
|
||||||
templates = Puppygirl._find_local_templates(tree) | self.templates
|
return self.renderer.render(self, tree)
|
||||||
|
|
||||||
for element in self.elements:
|
|
||||||
if hasattr(element, "name"):
|
|
||||||
for tag in tree.find_all(element.name):
|
|
||||||
new_tag = element.render(tag, templates)
|
|
||||||
if isinstance(new_tag, Iterable):
|
|
||||||
tag.extend(new_tag)
|
|
||||||
tag.unwrap()
|
|
||||||
else:
|
|
||||||
tag.replace_with(new_tag)
|
|
||||||
|
|
||||||
return tree
|
|
|
@ -1,16 +0,0 @@
|
||||||
from copy import copy
|
|
||||||
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from elements import Element
|
|
||||||
from puppytype import RenderableElement, TemplateDict
|
|
||||||
|
|
||||||
class Clonable[T = Self](Protocol):
|
|
||||||
def clone(self) -> T:
|
|
||||||
return copy(self)
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
|
||||||
class Renderable(Protocol):
|
|
||||||
def render(self, element: "Element", templates: "TemplateDict") -> "RenderableElement":
|
|
||||||
return element
|
|
|
@ -3,12 +3,10 @@ from typing import BinaryIO, Iterable, TextIO, TypeAlias, TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from string import Template
|
from string import Template
|
||||||
from bs4 import BeautifulSoup, Tag
|
from bs4 import BeautifulSoup, Tag
|
||||||
from puppygirl.puppygirl import TemplateInstance
|
from puppygirl.elements import Node
|
||||||
from puppygirl.elements import Element
|
|
||||||
|
|
||||||
Parsable: TypeAlias = "BeautifulSoup | Tag | str | bytes | TextIO | BinaryIO"
|
Parsable: TypeAlias = "BeautifulSoup | Tag | str | bytes | TextIO | BinaryIO"
|
||||||
ElementLike: TypeAlias = "BeautifulSoup | Tag | str | Element"
|
ElementLike: TypeAlias = "BeautifulSoup | Tag | str | Node"
|
||||||
ElementLikeList: TypeAlias = "Iterable[ElementLike]"
|
ElementLikeList: TypeAlias = "Iterable[ElementLike]"
|
||||||
RenderableElement: TypeAlias = "Element | Iterable[Element]"
|
RenderableElement: TypeAlias = "Node | Iterable[Node]"
|
||||||
Templatable: TypeAlias = "Template | TemplateInstance"
|
Templates: TypeAlias = "dict[str, Template]"
|
||||||
TemplateDict: TypeAlias = "dict[str, Template]"
|
|
||||||
|
|
59
puppygirl/renderer.py
Normal file
59
puppygirl/renderer.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from dataclasses import InitVar, dataclass, field
|
||||||
|
from importlib import resources
|
||||||
|
from typing import TYPE_CHECKING, Iterable, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from puppygirl.elements import IdAttr, TemplateName
|
||||||
|
from puppygirl.elements.shadow_root import Node
|
||||||
|
from puppygirl.puppytype import Templates
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from puppygirl import Puppygirl
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class Renderable(Protocol):
|
||||||
|
def render(self, tree: "BeautifulSoup") -> "BeautifulSoup":
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Renderer(Protocol):
|
||||||
|
def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup":
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClientSideRenderer(Renderer):
|
||||||
|
path: InitVar[str] = 'clientside.js'
|
||||||
|
injected_js: str = field(init = False)
|
||||||
|
|
||||||
|
def __post_init__(self, path: str):
|
||||||
|
with resources.open_text('puppygirl.assets', path) as f:
|
||||||
|
self.injected_js = f.read()
|
||||||
|
|
||||||
|
def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup":
|
||||||
|
head = tree.find("head")
|
||||||
|
script = tree.new_tag("script", attrs={"type": "text/javascript"})
|
||||||
|
script.append(self.injected_js)
|
||||||
|
head.append(script)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
class ServerSideRenderer(Renderer):
|
||||||
|
def _find_local_templates(puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "Templates":
|
||||||
|
templates = tree.find_all(TemplateName)
|
||||||
|
templates = filter(lambda t: t.has_attr(IdAttr), templates)
|
||||||
|
return puppygirl.__class__._create_template_dict(templates)
|
||||||
|
|
||||||
|
def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup":
|
||||||
|
templates = ServerSideRenderer._find_local_templates(puppygirl, tree) | puppygirl.templates
|
||||||
|
|
||||||
|
for element in puppygirl.elements:
|
||||||
|
if hasattr(element, "name"):
|
||||||
|
for tag in tree.find_all(element.name):
|
||||||
|
new_tag = element.render(Node(tag).clone(), templates)
|
||||||
|
|
||||||
|
if isinstance(new_tag, Iterable):
|
||||||
|
tag.extend(new_tag)
|
||||||
|
tag.unwrap()
|
||||||
|
else:
|
||||||
|
tag.replace_with(new_tag.value)
|
||||||
|
|
||||||
|
return tree
|
||||||
|
|
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[project]
|
||||||
|
name = "puppygirl"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"beautifulsoup4~=4.14.2"
|
||||||
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue