Skip to content

Commit e0a9e5b

Browse files
Modify component name parsing to preserve literal strings (#107)
1 parent b663974 commit e0a9e5b

File tree

4 files changed

+66
-19
lines changed

4 files changed

+66
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Changed
22+
23+
- Changed component name handling to preserve quotes, allowing literal string names to bypass dynamic resolution (e.g. `{% bird "button" %}` will always use "button" even if `button` exists in the context).
24+
2125
## [0.9.1]
2226

2327
### Fixed

docs/naming.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,24 @@ templates/
4949

5050
See [Organizing Components](organization.md) for detailed directory structure examples.
5151

52-
## Dynamic Names
52+
## Dynamic vs Literal Names
5353

54-
Component names can be dynamic, using template variables:
54+
Component names can be either dynamic or literal:
5555

5656
```htmldjango
57+
{# Dynamic name - resolves from context #}
5758
{% with component_name="icon.arrow-down" %}
5859
{% bird component_name / %}
5960
{% endwith %}
61+
62+
{# Literal name - always uses "button" #}
63+
{% bird "button" / %}
64+
{% bird 'button' / %}
6065
```
6166

62-
This is particularly useful when the component choice needs to be determined at runtime in your Django view.
67+
When using an unquoted name, django-bird will attempt to resolve it from the template context. This is useful when the component choice needs to be determined at runtime in your Django view.
68+
69+
Using quoted names (single or double quotes) ensures the literal string is used as the component name, bypassing context resolution. This is useful when you want to ensure a specific component is always used, even if a variable with the same name exists in the context.
6370

6471
## Template Resolution
6572

src/django_bird/templatetags/tags/bird.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424

2525
def do_bird(parser: Parser, token: Token) -> BirdNode:
2626
bits = token.split_contents()
27-
name = parse_bird_name(bits)
27+
if len(bits) == 1:
28+
msg = f"{TAG} tag requires at least one argument"
29+
raise template.TemplateSyntaxError(msg)
30+
name = bits[1]
2831
attrs = []
2932
only = False
3033

@@ -42,17 +45,6 @@ def do_bird(parser: Parser, token: Token) -> BirdNode:
4245
return BirdNode(name, attrs, nodelist, only)
4346

4447

45-
def parse_bird_name(bits: TagBits) -> str:
46-
if len(bits) == 1:
47-
msg = f"{TAG} tag requires at least one argument"
48-
raise template.TemplateSyntaxError(msg)
49-
50-
# {% bird name %}
51-
# {% bird 'name' %}
52-
# {% bird "name" %}
53-
return bits[1].strip("'\"")
54-
55-
5648
def parse_nodelist(bits: TagBits, parser: Parser) -> NodeList | None:
5749
# self-closing tag
5850
# {% bird name / %}

tests/templatetags/test_bird.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class TestTagParsing:
2727
"name,expected",
2828
[
2929
("button", "button"),
30-
("'button'", "button"),
31-
('"button"', "button"),
30+
("'button'", "'button'"),
31+
('"button"', '"button"'),
3232
("button.label", "button.label"),
3333
],
3434
)
@@ -202,7 +202,7 @@ def test_nested_name_templatetag(self, templates_dir, normalize_whitespace):
202202
name="button",
203203
content="""
204204
<button>
205-
Click me
205+
{{ slot }}
206206
</button>
207207
""",
208208
),
@@ -221,7 +221,7 @@ def test_nested_name_templatetag(self, templates_dir, normalize_whitespace):
221221
name="button",
222222
content="""
223223
<button>
224-
Click me
224+
{{ slot }}
225225
</button>
226226
""",
227227
),
@@ -233,6 +233,24 @@ def test_nested_name_templatetag(self, templates_dir, normalize_whitespace):
233233
template_context={"dynamic-name": "button"},
234234
expected="<button>Click me</button>",
235235
),
236+
TestComponentCase(
237+
description="Quoted component name should not be dynamic",
238+
component=TestComponent(
239+
name="button",
240+
content="""
241+
<button>
242+
{{ slot }}
243+
</button>
244+
""",
245+
),
246+
template_content="""
247+
{% bird "button" %}
248+
Click me
249+
{% endbird %}
250+
""",
251+
template_context={"button": "dynamic_name"},
252+
expected="<button>Click me</button>",
253+
),
236254
],
237255
ids=lambda x: x.description,
238256
)
@@ -248,6 +266,32 @@ def test_dynamic_name_template_context_templatetag(
248266

249267
assert normalize_whitespace(rendered) == test_case.expected
250268

269+
def test_dynamic_name_with_string(self, templates_dir, normalize_whitespace):
270+
button = TestComponent(
271+
name="button",
272+
content="""
273+
<button>
274+
{{ slot }}
275+
</button>
276+
""",
277+
)
278+
not_button = TestComponent(
279+
name="not_button",
280+
content="""
281+
<div>
282+
{{ slot }}
283+
</div>
284+
""",
285+
)
286+
button.create(templates_dir)
287+
not_button.create(templates_dir)
288+
289+
template = Template("{% bird 'button' %}Click me{% endbird %}")
290+
rendered = template.render(Context({"button": "not_button"}))
291+
292+
assert normalize_whitespace(rendered) == "<button>Click me</button>"
293+
assert normalize_whitespace(rendered) != "<div>Click me</div>"
294+
251295
def test_nonexistent_name_templatetag(self, templates_dir):
252296
template = Template("{% bird nonexistent %}Content{% endbird %}")
253297

0 commit comments

Comments
 (0)