issue with nested templates

This commit is contained in:
Rowan 2025-10-09 16:16:24 -04:00
parent 3deafd4d97
commit a2d0163949
3 changed files with 91 additions and 29 deletions

View file

@ -1,14 +1,15 @@
from dataclasses import dataclass from dataclasses import dataclass
import re
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .constants import TemplateAttr from .constants import TemplateAttr
from .shadow_root import ShadowRootMode from .shadow_root import ShadowRootMode, SingleNode
from ..renderer import Query, Renderable from ..renderer import Query, RenderState, Renderable
from .shadow_root import Node
if TYPE_CHECKING: if TYPE_CHECKING:
from .. import Puppygirl from .. import Puppygirl
from .shadow_root import Node from ..puppytype import RenderableElement
from ..puppytype import RenderableElement, Templates
@dataclass @dataclass
class PuppygirlHost(Renderable): class PuppygirlHost(Renderable):
@ -19,16 +20,40 @@ class PuppygirlHost(Renderable):
return "pg-host" return "pg-host"
def query(self) -> Query: 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": def render(self, node: "Node", state: "RenderState") -> "RenderableElement":
template = templates[node[TemplateAttr]] 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 = node.attach_shadow(ShadowRootMode.Open)
shadow_root.append_child(template.clone()) shadow_root.append_child(template.clone())
return node 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 @dataclass
class PuppygirlPart(Renderable): class PuppygirlPart(Renderable):
puppygirl: "Puppygirl" puppygirl: "Puppygirl"
@ -40,14 +65,20 @@ class PuppygirlPart(Renderable):
def query(self) -> Query: def query(self) -> Query:
return Query(self.name, attrs={ "for": True }) 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) template = node.parent.find("template", recursive=False)
if template is None: if template is None:
return node return node
part_name = node["for"] parts = template(part=part_name, recusive=False)
parts = template.find_all(attrs={ "part": part_name }) 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 = { attrs = {
attr: value attr: value
@ -58,8 +89,14 @@ class PuppygirlPart(Renderable):
for attr, value in attrs.items(): for attr, value in attrs.items():
for part in parts: for part in parts:
part[attr] = value part[attr] = value
def render(self, node: "Node", state: "RenderState") -> "RenderableElement":
part_name = node["for"]
self.replace_part(node, part_name)
return [] return None

View file

@ -22,12 +22,16 @@ class Node(Clonable):
def from_path(path: str) -> Self: def from_path(path: str) -> Self:
with open(path, "r") as f: with open(path, "r") as f:
soup = BeautifulSoup(f.read(), 'html.parser') soup = BeautifulSoup(f.read(), 'html.parser')
nodes = soup.find_all() return Node.from_tag(soup)
match len(nodes): def from_tag(tag: Tag | Iterable[Tag] | BeautifulSoup) -> Self:
case 0: return EmptyNode() if isinstance(tag, Tag):
case 1: return SingleNode(nodes[0]) return SingleNode(tag)
case _: return NodeCollection(soup)
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): def attach_shadow(self, shadow_root_mode: ShadowRootMode, clonable: Optional[bool] = None, delegates_focus: Optional[bool] = None, serializable: Optional[bool] = None):
return ShadowRoot( return ShadowRoot(
@ -50,9 +54,16 @@ class Node(Clonable):
def insert(self, *args, **kwargs): def insert(self, *args, **kwargs):
return self.value.insert(*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: def has_attr(self, attr: str) -> bool:
return self.value.has_attr(attr) 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: def prettify(self) -> str:
return self.value.prettify() return self.value.prettify()
@ -65,6 +76,9 @@ class EmptyNode(Node):
class NodeCollection(Node): class NodeCollection(Node):
nodes: Iterable[Tag] nodes: Iterable[Tag]
def __iter__(self):
return self.nodes
@property @property
def value(self): def value(self):
return self.nodes return self.nodes
@ -100,7 +114,7 @@ class SingleNode(Node):
return SingleNode(copy(self.value)) return SingleNode(copy(self.value))
@dataclass @dataclass
class Template(Clonable): class Template(Node):
_value: Node _value: Node
@property @property

View file

@ -1,12 +1,13 @@
from multiprocessing import Pool
from dataclasses import InitVar, dataclass, field from dataclasses import InitVar, dataclass, field
from importlib import resources 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 bs4 import Tag
from puppygirl.elements import IdAttr, TemplateName from puppygirl.elements import IdAttr, TemplateName
from puppygirl.elements.shadow_root import Node, SingleNode from puppygirl.elements.shadow_root import Node, SingleNode, Template
from puppygirl.puppytype import Templates from puppygirl.puppytype import RenderableElement, Templates
if TYPE_CHECKING: if TYPE_CHECKING:
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@ -23,13 +24,17 @@ class Query:
def query(self, tree: "BeautifulSoup") -> Iterable[Tag]: def query(self, tree: "BeautifulSoup") -> Iterable[Tag]:
return tree.find_all(*self._args, **self._kwargs) return tree.find_all(*self._args, **self._kwargs)
@dataclass
class RenderState:
templates: "Templates"
@runtime_checkable @runtime_checkable
class Renderable(Protocol): class Renderable(Protocol):
def query(self) -> Query: def query(self) -> Query:
return Query() return Query()
def render(self, tree: "BeautifulSoup") -> "BeautifulSoup": def render(self, node: "Node", state: RenderState) -> "RenderableElement":
return tree return node
class Renderer(Protocol): class Renderer(Protocol):
def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup": def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup":
@ -60,24 +65,30 @@ class ServerSideRenderer(Renderer):
def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup": def render(self, puppygirl: "Puppygirl", tree: "BeautifulSoup") -> "BeautifulSoup":
templates = ServerSideRenderer._find_local_templates(puppygirl, tree) | puppygirl.templates templates = ServerSideRenderer._find_local_templates(puppygirl, tree) | puppygirl.templates
state = RenderState(templates)
for element in puppygirl.elements: for element in puppygirl.elements:
if hasattr(element, "name"): if hasattr(element, "name"):
query = element.query() query = element.query()
tags = query.query(tree) tags = query.query(tree)
tags.reverse() tags.reverse()
for tag in tags: for tag in tags:
node = SingleNode(tag)
new_tag = element.render( new_tag = element.render(
SingleNode(tag), node,
# SingleNode(tag).clone(), state
templates
) )
if isinstance(new_tag, Iterable): if new_tag is None or new_tag.value == tag:
tag.extend(new_tag) continue
tag.unwrap()
else: 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 return tree