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