puppygirl-py/puppygirl/renderer.py

94 lines
2.7 KiB
Python

from multiprocessing import Pool
from dataclasses import InitVar, dataclass, field
from importlib import resources
from typing import TYPE_CHECKING, Any, Iterable, Protocol, runtime_checkable
from bs4 import Tag
from puppygirl.elements import IdAttr, TemplateName
from puppygirl.elements.shadow_root import Node, SingleNode, Template
from puppygirl.puppytype import RenderableElement, Templates
if TYPE_CHECKING:
from bs4 import BeautifulSoup
from puppygirl import Puppygirl
class Query:
_args: Iterable[Any]
_kwargs: dict[str, Any]
def __init__(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
def query(self, tree: "BeautifulSoup") -> Iterable[Tag]:
return tree.find_all(*self._args, **self._kwargs)
@dataclass
class RenderState:
templates: "Templates"
@runtime_checkable
class Renderable(Protocol):
def query(self) -> Query:
return Query()
def render(self, node: "Node", state: RenderState) -> "RenderableElement":
return node
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
state = RenderState(templates)
for element in puppygirl.elements:
if hasattr(element, "name"):
query = element.query()
tags = query.query(tree)
tags.reverse()
for tag in tags:
node = SingleNode(tag)
new_tag = element.render(
node,
state
)
if new_tag is None or new_tag.value == tag:
continue
else:
node.replace(new_tag)
# elif isinstance(new_tag, Iterable):
# tag.extend(new_tag.value)
# tag.unwrap()
# else:
# tag.replace_with(new_tag.value)
return tree