From a2d01639491570a8c3a0485127a56dab458e15c7 Mon Sep 17 00:00:00 2001 From: rowan Date: Thu, 9 Oct 2025 16:16:24 -0400 Subject: [PATCH] issue with nested templates --- puppygirl/elements/host.py | 59 +++++++++++++++++++++++++------ puppygirl/elements/shadow_root.py | 26 ++++++++++---- puppygirl/renderer.py | 35 +++++++++++------- 3 files changed, 91 insertions(+), 29 deletions(-) diff --git a/puppygirl/elements/host.py b/puppygirl/elements/host.py index c0daa5a..9d52903 100644 --- a/puppygirl/elements/host.py +++ b/puppygirl/elements/host.py @@ -1,14 +1,15 @@ from dataclasses import dataclass +import re from typing import TYPE_CHECKING from .constants import TemplateAttr -from .shadow_root import ShadowRootMode -from ..renderer import Query, Renderable +from .shadow_root import ShadowRootMode, SingleNode +from ..renderer import Query, RenderState, Renderable +from .shadow_root import Node if TYPE_CHECKING: from .. import Puppygirl - from .shadow_root import Node - from ..puppytype import RenderableElement, Templates + from ..puppytype import RenderableElement @dataclass class PuppygirlHost(Renderable): @@ -19,16 +20,40 @@ class PuppygirlHost(Renderable): return "pg-host" def query(self) -> Query: - return Query(self.name, attrs={ "template": True }) + return Query(self.name, attrs={ "template": True }, recursive=True) - def render(self, node: "Node", templates: "Templates") -> "RenderableElement": - template = templates[node[TemplateAttr]] + def render(self, node: "Node", state: "RenderState") -> "RenderableElement": + node = node.clone() + + template = state.templates.get(node.get_attr(TemplateAttr)).clone() + + for tag in self.query().query(template): + result = self.render(SingleNode(tag), state) + Node.from_tag(tag).replace(result) shadow_root = node.attach_shadow(ShadowRootMode.Open) shadow_root.append_child(template.clone()) return node +def get_exported_name(part: str, mapping: str) -> str | None: + mapping = mapping.strip() + map_len = len(mapping) + + if map_len == 0: return None + + index = mapping.index(part) + end = index + len(part) + + if end >= map_len or mapping[end] == ":": + return part + else: + try: + map_end = mapping.index(",", end) + return mapping[end + 1:map_end] + except: + return mapping[end + 1:] + @dataclass class PuppygirlPart(Renderable): puppygirl: "Puppygirl" @@ -40,14 +65,20 @@ class PuppygirlPart(Renderable): def query(self) -> Query: return Query(self.name, attrs={ "for": True }) - def render(self, node: "Node", templates: "Templates") -> "RenderableElement": + def replace_part(self, node: "Node", part_name: str): template = node.parent.find("template", recursive=False) if template is None: return node - part_name = node["for"] - parts = template.find_all(attrs={ "part": part_name }) + parts = template(part=part_name, recusive=False) + exports = template(exportparts=re.compile(part_name), recursive=False) + + for export in exports: + mapping = get_exported_name(part_name, export["exportparts"]) + if mapping is None: continue + + self.replace_part(SingleNode(export), part_name) attrs = { attr: value @@ -58,8 +89,14 @@ class PuppygirlPart(Renderable): for attr, value in attrs.items(): for part in parts: part[attr] = value + + + def render(self, node: "Node", state: "RenderState") -> "RenderableElement": + part_name = node["for"] + + self.replace_part(node, part_name) - return [] + return None diff --git a/puppygirl/elements/shadow_root.py b/puppygirl/elements/shadow_root.py index c2260ee..8de093f 100644 --- a/puppygirl/elements/shadow_root.py +++ b/puppygirl/elements/shadow_root.py @@ -22,12 +22,16 @@ class Node(Clonable): def from_path(path: str) -> Self: with open(path, "r") as f: soup = BeautifulSoup(f.read(), 'html.parser') - nodes = soup.find_all() + return Node.from_tag(soup) - match len(nodes): - case 0: return EmptyNode() - case 1: return SingleNode(nodes[0]) - case _: return NodeCollection(soup) + def from_tag(tag: Tag | Iterable[Tag] | BeautifulSoup) -> Self: + if isinstance(tag, Tag): + return SingleNode(tag) + + match len(tag): + case 0: return EmptyNode() + case 1: return SingleNode(tag[0]) + case _: return NodeCollection(tag) def attach_shadow(self, shadow_root_mode: ShadowRootMode, clonable: Optional[bool] = None, delegates_focus: Optional[bool] = None, serializable: Optional[bool] = None): return ShadowRoot( @@ -50,9 +54,16 @@ class Node(Clonable): def insert(self, *args, **kwargs): return self.value.insert(*args, **kwargs) + def get_attr(self, attr: str) -> Optional[str]: + return self.value.get(attr) + def has_attr(self, attr: str) -> bool: return self.value.has_attr(attr) + def replace(self, node: "Node | Iterable[Node]"): + self.value.insert_before(node.value) + self.value.decompose() + def prettify(self) -> str: return self.value.prettify() @@ -65,6 +76,9 @@ class EmptyNode(Node): class NodeCollection(Node): nodes: Iterable[Tag] + def __iter__(self): + return self.nodes + @property def value(self): return self.nodes @@ -100,7 +114,7 @@ class SingleNode(Node): return SingleNode(copy(self.value)) @dataclass -class Template(Clonable): +class Template(Node): _value: Node @property diff --git a/puppygirl/renderer.py b/puppygirl/renderer.py index 69472a2..eadd232 100644 --- a/puppygirl/renderer.py +++ b/puppygirl/renderer.py @@ -1,12 +1,13 @@ +from multiprocessing import Pool from dataclasses import InitVar, dataclass, field from importlib import resources -from typing import TYPE_CHECKING, Any, Iterable, Protocol, Self, runtime_checkable +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 -from puppygirl.puppytype import Templates +from puppygirl.elements.shadow_root import Node, SingleNode, Template +from puppygirl.puppytype import RenderableElement, Templates if TYPE_CHECKING: from bs4 import BeautifulSoup @@ -23,13 +24,17 @@ class Query: 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, tree: "BeautifulSoup") -> "BeautifulSoup": - return tree + def render(self, node: "Node", state: RenderState) -> "RenderableElement": + return node class Renderer(Protocol): def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup": @@ -60,24 +65,30 @@ class ServerSideRenderer(Renderer): 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( - SingleNode(tag), - # SingleNode(tag).clone(), - templates + node, + state ) - if isinstance(new_tag, Iterable): - tag.extend(new_tag) - tag.unwrap() + if new_tag is None or new_tag.value == tag: + continue else: - tag.replace_with(new_tag.value) + 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