Skip to content

Commit 11d2ea8

Browse files
committed
Output warning if the component HTML doesn't appear to be well formed.
1 parent 7f10a3e commit 11d2ea8

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

django_unicorn/components/unicorn_template_response.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
import re
3+
from collections import deque
24

35
from django.template.response import TemplateResponse
46

@@ -19,6 +21,45 @@
1921
logger = logging.getLogger(__name__)
2022

2123

24+
# https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
25+
EMPTY_ELEMENTS = (
26+
"<area>",
27+
"<base>",
28+
"<br>",
29+
"<col>",
30+
"<embed>",
31+
"<hr>",
32+
"<img>",
33+
"<input>",
34+
"<link>",
35+
"<meta>",
36+
"<param>",
37+
"<source>",
38+
"<track>",
39+
"<wbr>",
40+
)
41+
42+
43+
def is_html_well_formed(html: str) -> bool:
44+
"""
45+
Whether the passed-in HTML is missing any closing elements which can cause issues when rendering.
46+
"""
47+
48+
tag_list = re.split("(<[^>!]*>)", html)[1::2]
49+
stack = deque()
50+
51+
for tag in tag_list:
52+
if "/" not in tag:
53+
cleaned_tag = re.sub(r"(<(\w+)[^>!]*>)", r"<\2>", tag)
54+
55+
if cleaned_tag not in EMPTY_ELEMENTS:
56+
stack.append(cleaned_tag)
57+
elif len(stack) > 0 and (tag.replace("/", "") == stack[len(stack) - 1]):
58+
stack.pop()
59+
60+
return len(stack) == 0
61+
62+
2263
class UnsortedAttributes(HTMLFormatter):
2364
"""
2465
Prevent beautifulsoup from re-ordering attributes.
@@ -68,6 +109,11 @@ def render(self):
68109

69110
content = response.content.decode("utf-8")
70111

112+
if not is_html_well_formed(content):
113+
logger.warning(
114+
f"The HTML in '{self.component.component_name}' appears to be missing a closing tag. That can potentially cause errors in Unicorn."
115+
)
116+
71117
frontend_context_variables = self.component.get_frontend_context_variables()
72118
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
73119
checksum = generate_checksum(orjson.dumps(frontend_context_variables_dict))

example/project/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,10 @@
145145
"level": "INFO",
146146
"propagate": False,
147147
},
148+
"django_unicorn": {
149+
"handlers": ["console"],
150+
"level": "DEBUG",
151+
"propagate": False,
152+
},
148153
},
149154
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from django_unicorn.components.unicorn_template_response import is_html_well_formed
2+
3+
4+
def test_is_html_well_formed():
5+
html = """
6+
<div>
7+
something
8+
</div>
9+
"""
10+
actual = is_html_well_formed(html)
11+
12+
assert actual is True
13+
14+
15+
def test_is_html_well_formed_comment():
16+
html = """
17+
18+
<!-- some comment here -->
19+
20+
<div>
21+
something
22+
</div>
23+
"""
24+
actual = is_html_well_formed(html)
25+
26+
assert actual is True
27+
28+
29+
def test_is_html_well_formed_p():
30+
html = """
31+
32+
<p>
33+
something
34+
<br />
35+
</p>
36+
"""
37+
actual = is_html_well_formed(html)
38+
39+
assert actual is True
40+
41+
42+
def test_is_html_well_formed_missing_internal():
43+
html = """
44+
<p>
45+
<div>
46+
something
47+
<br />
48+
</p>
49+
"""
50+
actual = is_html_well_formed(html)
51+
52+
assert actual is False
53+
54+
55+
def test_is_html_well_formed_multiple():
56+
html = """
57+
<div>
58+
<div>something</div>
59+
</div>
60+
"""
61+
actual = is_html_well_formed(html)
62+
63+
assert actual is True
64+
65+
66+
def test_is_html_well_formed_missing():
67+
html = """
68+
<div>
69+
something
70+
71+
"""
72+
actual = is_html_well_formed(html)
73+
74+
assert actual is False
75+
76+
77+
def test_is_html_well_formed_invalid():
78+
html = """
79+
<div>
80+
something
81+
</di>
82+
83+
"""
84+
actual = is_html_well_formed(html)
85+
86+
assert actual is False
87+
88+
89+
def test_is_html_well_formed_no_slash():
90+
html = """
91+
<div>
92+
something
93+
<div>
94+
95+
"""
96+
actual = is_html_well_formed(html)
97+
98+
assert actual is False
99+
100+
101+
def test_is_well_formed_more():
102+
html = """
103+
<div>
104+
<button unicorn:click="$reset">Reset the component</button>
105+
<button onclick="document.getElementById('name').value = 'asdf';">Plain JS set value on key1</button>
106+
<button onclick="Unicorn.trigger('unicorn.components.hello_world.HelloWorldView', 'key1')">Trigger key1</button>
107+
<br />
108+
<br />
109+
110+
<label></label>
111+
<input unicorn:model="name" type="text" id="name" unicorn:key="key1">
112+
113+
Hello, {{ name|default:'World' }}!
114+
115+
<div>
116+
Request path context variable: '{{ request.path }}'
117+
</div>
118+
</div>
119+
"""
120+
121+
actual = is_html_well_formed(html)
122+
123+
assert actual is True

0 commit comments

Comments
 (0)