Skip to content

Commit d35d517

Browse files
reorganize tags to dedicated directory (#48)
* reorganize tags to dedicated directory * move tests
1 parent cf84c9f commit d35d517

File tree

7 files changed

+381
-339
lines changed

7 files changed

+381
-339
lines changed
Lines changed: 4 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,222 +1,13 @@
11
# pyright: reportAny=false
22
from __future__ import annotations
33

4-
from typing import Any
5-
from typing import cast
6-
74
from django import template
8-
from django.template.base import NodeList
9-
from django.template.base import Parser
10-
from django.template.base import Token
11-
from django.template.context import Context
12-
from django.template.loader import select_template
13-
from django.utils.safestring import SafeString
14-
from django.utils.safestring import mark_safe
155

16-
from django_bird._typing import override
17-
from django_bird.conf import app_settings
6+
from .tags import do_bird
7+
from .tags import do_slot
188

199
register = template.Library()
2010

2111

22-
@register.tag(name="bird")
23-
def do_bird(parser: Parser, token: Token) -> BirdNode:
24-
bits = token.split_contents()
25-
26-
if len(bits) < 2:
27-
msg = f"{token.contents.split()[0]} tag requires at least one argument"
28-
raise template.TemplateSyntaxError(msg)
29-
30-
# {% bird name %}
31-
# {% bird 'name' %}
32-
# {% bird "name" %}
33-
name = bits[1].strip("'\"")
34-
attrs = bits[2:]
35-
36-
# self-closing tag
37-
# {% bird name / %}
38-
if len(attrs) > 0 and attrs[-1] == "/":
39-
nodelist = None
40-
else:
41-
nodelist = parser.parse(("endbird",))
42-
parser.delete_first_token()
43-
44-
return BirdNode(name, attrs, nodelist)
45-
46-
47-
class BirdNode(template.Node):
48-
def __init__(self, name: str, attrs: list[str], nodelist: NodeList | None) -> None:
49-
self.name = name
50-
self.attrs = attrs
51-
self.nodelist = nodelist
52-
self.default_slot = "default"
53-
54-
@override
55-
def render(self, context: Context) -> SafeString:
56-
component_name = self.get_component_name(context)
57-
component_context = self.get_component_context_data(context)
58-
template_names = self.get_template_names(component_name)
59-
template = select_template(template_names)
60-
return template.render(component_context)
61-
62-
def get_component_name(self, context: Context) -> str:
63-
try:
64-
name = template.Variable(self.name).resolve(context)
65-
except template.VariableDoesNotExist:
66-
name = self.name
67-
return name
68-
69-
def get_component_context_data(self, context: Context) -> dict[str, Any]:
70-
rendered_slots = self.render_slots(context)
71-
flat_attrs = self.flatten_attrs(context)
72-
default_slot = rendered_slots.get(self.default_slot) or context.get("slot")
73-
return {
74-
"attrs": mark_safe(flat_attrs),
75-
"slot": mark_safe(default_slot),
76-
"slots": {
77-
name: mark_safe(content) for name, content in rendered_slots.items()
78-
},
79-
}
80-
81-
def render_slots(self, context: Context) -> dict[str, str]:
82-
if self.nodelist is None:
83-
return {}
84-
85-
contents: dict[str, list[str]] = {self.default_slot: []}
86-
active_slot = self.default_slot
87-
88-
for node in self.nodelist:
89-
if isinstance(node, SlotNode):
90-
active_slot = node.name
91-
contents.setdefault(active_slot, [])
92-
else:
93-
active_slot = self.default_slot
94-
95-
rendered_content = node.render(context)
96-
contents[active_slot].append(rendered_content)
97-
98-
if (
99-
all(not content for content in contents[self.default_slot])
100-
and "slot" in context
101-
):
102-
contents[self.default_slot] = [context["slot"]]
103-
104-
return {
105-
slot: template.Template("".join(content)).render(context)
106-
for slot, content in contents.items()
107-
}
108-
109-
def flatten_attrs(self, context: Context) -> str:
110-
attrs: dict[str, str | None | bool] = {}
111-
112-
for attr in self.attrs:
113-
if "=" in attr:
114-
key, value = attr.split("=", 1)
115-
attrs[key] = template.Variable(value).resolve(context)
116-
else:
117-
attrs[attr] = True
118-
119-
return " ".join(
120-
key if isinstance(value, bool) and value else f'{key}="{value}"'
121-
for key, value in attrs.items()
122-
)
123-
124-
def get_template_names(self, name: str) -> list[str]:
125-
"""
126-
Generate a list of potential template names for a component.
127-
128-
The function searches for templates in the following order (from most specific to most general):
129-
130-
1. In a subdirectory named after the component, using the component name
131-
2. In the same subdirectory, using a fallback 'index.html'
132-
3. In parent directory for nested components
133-
4. In the base component directory, using the full component name
134-
135-
The order of names is important as it determines the template resolution priority.
136-
This order allows for both direct matches and hierarchical component structures,
137-
with more specific paths taking precedence over more general ones.
138-
139-
This order allows for:
140-
- Single file components
141-
- Multi-part components
142-
- Specific named files within component directories
143-
- Fallback default files for components
144-
145-
For example:
146-
- For an "input" component, the ordering would be:
147-
1. `{component_dir}/input/input.html`
148-
2. `{component_dir}/input/index.html`
149-
3. `{component_dir}/input.html`
150-
- For an "input.label" component:
151-
1. `{component_dir}/input/label/label.html`
152-
2. `{component_dir}/input/label/index.html`
153-
3. `{component_dir}/input/label.html`
154-
4. `{component_dir}/input.label.html`
155-
156-
Returns:
157-
list[str]: A list of potential template names in resolution order.
158-
"""
159-
template_names = []
160-
component_dirs = list(dict.fromkeys([*app_settings.COMPONENT_DIRS, "bird"]))
161-
162-
name_parts = name.split(".")
163-
path_name = "/".join(name_parts)
164-
165-
for component_dir in component_dirs:
166-
potential_names = [
167-
f"{component_dir}/{path_name}/{name_parts[-1]}.html",
168-
f"{component_dir}/{path_name}/index.html",
169-
f"{component_dir}/{path_name}.html",
170-
f"{component_dir}/{self.name}.html",
171-
]
172-
template_names.extend(potential_names)
173-
174-
return list(dict.fromkeys(template_names))
175-
176-
177-
@register.tag("bird:slot")
178-
def do_slot(parser: Parser, token: Token) -> SlotNode:
179-
bits = token.split_contents()
180-
name = parse_slot_name(bits)
181-
nodelist = parser.parse(("endbird:slot",))
182-
parser.delete_first_token()
183-
return SlotNode(name, nodelist)
184-
185-
186-
def parse_slot_name(tag_args: list[str]) -> str:
187-
if len(tag_args) == 1:
188-
return "default"
189-
elif len(tag_args) == 2:
190-
name = tag_args[1]
191-
if name.startswith("name="):
192-
name = name.split("=")[1]
193-
else:
194-
name = name
195-
return name.strip("'\"")
196-
else:
197-
raise template.TemplateSyntaxError(
198-
"slot tag requires either no arguments, one argument, or 'name=\"slot_name\"'"
199-
)
200-
201-
202-
class SlotNode(template.Node):
203-
def __init__(self, name: str, nodelist: NodeList):
204-
self.name = name
205-
self.nodelist = nodelist
206-
207-
@override
208-
def render(self, context: Context) -> SafeString:
209-
default_content: str = self.nodelist.render(context)
210-
slots = context.get("slots")
211-
212-
if not slots or not isinstance(slots, dict):
213-
slot_content = default_content
214-
else:
215-
slots_dict = cast(dict[str, str], slots)
216-
slot_content = slots_dict.get(self.name, default_content)
217-
218-
# Recursively process the slot content
219-
t = template.Template(slot_content)
220-
content = t.render(context)
221-
222-
return mark_safe(content)
12+
register.tag("bird", do_bird)
13+
register.tag("bird:slot", do_slot)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from .bird import do_bird
4+
from .slot import do_slot
5+
6+
__all__ = [
7+
"do_bird",
8+
"do_slot",
9+
]

0 commit comments

Comments
 (0)