Skip to content

Commit b6d5604

Browse files
committed
Handle component templates with comments or blank strings before a root element.
1 parent c4e5ace commit b6d5604

File tree

4 files changed

+70
-17
lines changed

4 files changed

+70
-17
lines changed

django_unicorn/components/unicorn_template_response.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def render(self):
6868
checksum = generate_checksum(orjson.dumps(frontend_context_variables_dict))
6969

7070
soup = BeautifulSoup(content, features="html.parser")
71-
root_element = UnicornTemplateResponse._get_root_element(soup)
71+
root_element = get_root_element(soup)
7272
root_element["unicorn:id"] = self.component.component_id
7373
root_element["unicorn:name"] = self.component.component_name
7474
root_element["unicorn:key"] = self.component.component_key
@@ -122,23 +122,23 @@ def render(self):
122122

123123
return response
124124

125-
@staticmethod
126-
def _get_root_element(soup: BeautifulSoup) -> Tag:
127-
"""
128-
Gets the first element.
129-
130-
Returns:
131-
BeautifulSoup element.
132-
133-
Raises an Exception if an element cannot be found.
134-
"""
135-
for element in soup.contents:
136-
if element.name:
137-
return element
138-
139-
raise Exception("No root element found")
140-
141125
@staticmethod
142126
def _desoupify(soup):
143127
soup.smooth()
144128
return soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
129+
130+
131+
def get_root_element(soup: BeautifulSoup) -> Tag:
132+
"""
133+
Gets the first tag element.
134+
135+
Returns:
136+
BeautifulSoup tag element.
137+
138+
Raises an Exception if an element cannot be found.
139+
"""
140+
for element in soup.contents:
141+
if isinstance(element, Tag) and element.name:
142+
return element
143+
144+
raise Exception("No root element found")

django_unicorn/views/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from bs4 import BeautifulSoup
1414

1515
from django_unicorn.components import UnicornView
16+
from django_unicorn.components.unicorn_template_response import get_root_element
1617
from django_unicorn.decorators import timed
1718
from django_unicorn.errors import RenderNotModified, UnicornCacheError, UnicornViewError
1819
from django_unicorn.serializer import dumps, loads
@@ -217,6 +218,10 @@ def _process_component_request(
217218
):
218219
raise RenderNotModified()
219220

221+
# Make sure that partials with comments or blank lines before the root element only return the root element
222+
soup = BeautifulSoup(rendered_component, features="html.parser")
223+
rendered_component = str(get_root_element(soup))
224+
220225
res.update(
221226
{"dom": rendered_component, "hash": hash,}
222227
)

example/unicorn/templates/unicorn/objects.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- test comment -->
2+
13
<div>
24
<div>
35
<label>Dictionaries (original)</label>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
from bs4 import BeautifulSoup
3+
4+
from django_unicorn.components.unicorn_template_response import get_root_element
5+
6+
7+
def test_get_root_element():
8+
expected = "<div>test</div>"
9+
10+
component_html = "<div>test</div>"
11+
soup = BeautifulSoup(component_html, features="html.parser")
12+
actual = get_root_element(soup)
13+
14+
assert str(actual) == expected
15+
16+
17+
def test_get_root_element_with_comment():
18+
expected = "<div>test</div>"
19+
20+
component_html = "<!-- some comment --><div>test</div>"
21+
soup = BeautifulSoup(component_html, features="html.parser")
22+
actual = get_root_element(soup)
23+
24+
assert str(actual) == expected
25+
26+
27+
def test_get_root_element_with_blank_string():
28+
expected = "<div>test</div>"
29+
30+
component_html = "\n<div>test</div>"
31+
soup = BeautifulSoup(component_html, features="html.parser")
32+
actual = get_root_element(soup)
33+
34+
assert str(actual) == expected
35+
36+
37+
def test_get_root_element_no_element():
38+
expected = "<div>test</div>"
39+
40+
component_html = "\n"
41+
soup = BeautifulSoup(component_html, features="html.parser")
42+
43+
with pytest.raises(Exception):
44+
actual = get_root_element(soup)
45+
46+
assert str(actual) == expected

0 commit comments

Comments
 (0)