Coverage for tdom / parser_test.py: 96%
203 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-03 21:23 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-03 21:23 +0000
1from string.templatelib import Interpolation, Template
3import pytest
5from .parser import TemplateParser
6from .placeholders import TemplateRef, make_placeholder_config
7from .tnodes import (
8 TComment,
9 TComponent,
10 TDocumentType,
11 TElement,
12 TFragment,
13 TInterpolatedAttribute,
14 TLiteralAttribute,
15 TSpreadAttribute,
16 TTemplatedAttribute,
17 TText,
18)
21def test_parse_mixed_literal_content():
22 node = TemplateParser.parse(
23 t"<!DOCTYPE html>"
24 t"<!-- Comment -->"
25 t'<div class="container">'
26 t"Hello, <br class='funky' />world <!-- neato -->!"
27 t"</div>"
28 )
29 assert node == TFragment(
30 children=(
31 TDocumentType("html"),
32 TComment.literal(" Comment "),
33 TElement(
34 "div",
35 attrs=(TLiteralAttribute("class", "container"),),
36 children=(
37 TText.literal("Hello, "),
38 TElement("br", attrs=(TLiteralAttribute("class", "funky"),)),
39 TText.literal("world "),
40 TComment.literal(" neato "),
41 TText.literal("!"),
42 ),
43 ),
44 )
45 )
48#
49# Text
50#
51def test_parse_empty():
52 node = TemplateParser.parse(t"")
53 assert node == TFragment()
56def test_parse_text():
57 node = TemplateParser.parse(t"Hello, world!")
58 assert node == TText.literal("Hello, world!")
61def test_parse_text_multiline():
62 node = TemplateParser.parse(t"""Hello, world!
63 Hello, moon!
64Hello, sun!
65""")
66 assert node == TText.literal("""Hello, world!
67 Hello, moon!
68Hello, sun!
69""")
72def test_parse_text_with_entities():
73 node = TemplateParser.parse(t"a < b")
74 assert node == TText.literal("a < b")
77def test_parse_text_with_template_singleton():
78 greeting = "Hello, World!"
79 node = TemplateParser.parse(t"{greeting}")
80 assert node == TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,)))
83def test_parse_text_with_template():
84 who = "World"
85 node = TemplateParser.parse(t"Hello, {who}!")
86 assert node == TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,)))
89#
90# Elements
91#
92def test_parse_void_element():
93 node = TemplateParser.parse(t"<br>")
94 assert node == TElement("br")
97def test_parse_void_element_self_closed():
98 node = TemplateParser.parse(t"<br />")
99 assert node == TElement("br")
102def test_parse_uppercase_void_element():
103 node = TemplateParser.parse(t"<BR>")
104 assert node == TElement("br")
107def test_parse_standard_element_with_text():
108 node = TemplateParser.parse(t"<div>Hello, world!</div>")
109 assert node == TElement("div", children=(TText.literal("Hello, world!"),))
112def test_parse_nested_elements():
113 node = TemplateParser.parse(t"<div><span>Nested</span> content</div>")
114 assert node == TElement(
115 "div",
116 children=(
117 TElement("span", children=(TText.literal("Nested"),)),
118 TText.literal(" content"),
119 ),
120 )
123def test_parse_element_with_template():
124 who = "World"
125 node = TemplateParser.parse(t"<div>Hello, {who}!</div>")
126 assert node == TElement(
127 "div",
128 children=(TText(ref=TemplateRef(strings=("Hello, ", "!"), i_indexes=(0,))),),
129 )
132def test_parse_element_with_template_singleton():
133 greeting = "Hello, World!"
134 node = TemplateParser.parse(t"<div>{greeting}</div>")
135 assert node == TElement(
136 "div", children=(TText(ref=TemplateRef(strings=("", ""), i_indexes=(0,))),)
137 )
140def test_parse_multiple_voids():
141 node = TemplateParser.parse(t"<br><hr><hr /><hr /><br /><br><br>")
142 assert node == TFragment(
143 children=(
144 TElement("br"),
145 TElement("hr"),
146 TElement("hr"),
147 TElement("hr"),
148 TElement("br"),
149 TElement("br"),
150 TElement("br"),
151 )
152 )
155def test_parse_text_entities():
156 node = TemplateParser.parse(t"<p></p></p>")
157 assert node == TElement(
158 "p",
159 children=(TText.literal("</p>"),),
160 )
163def test_parse_script_tag_content():
164 node = TemplateParser.parse(
165 t"<script>if (a < b && c > d) { alert('wow'); } </script>"
166 )
167 assert node == TElement(
168 "script",
169 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
170 )
173def test_parse_script_with_entities():
174 # The <script> tag (and <style>) tag uses the CDATA content model.
175 node = TemplateParser.parse(t"<script>var x = 'a & b';</script>")
176 assert node == TElement(
177 "script",
178 children=(TText.literal("var x = 'a & b';"),),
179 ), "Entities SHOULD NOT be evaluated in scripts."
182def test_parse_textarea_tag_content():
183 node = TemplateParser.parse(
184 t"<textarea>if (a < b && c > d) { alert('wow'); } </textarea>"
185 )
186 assert node == TElement(
187 "textarea",
188 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
189 )
192def test_parse_textarea_with_entities():
193 # The <textarea> (and <title>) tag uses the RCDATA content model.
194 node = TemplateParser.parse(t"<textarea>var x = 'a & b';</textarea>")
195 assert node == TElement(
196 "textarea",
197 children=(TText.literal("var x = 'a & b';"),),
198 ), "Entities SHOULD be evaluated in textarea/title."
201def test_parse_title_unusual():
202 node = TemplateParser.parse(t"<title>My & Awesome <Site></title>")
203 assert node == TElement(
204 "title",
205 children=(TText.literal("My & Awesome <Site>"),),
206 )
209def test_parse_mismatched_tags():
210 with pytest.raises(ValueError):
211 _ = TemplateParser.parse(t"<div><span>Mismatched</div></span>")
214def test_parse_unclosed_tag():
215 with pytest.raises(ValueError):
216 _ = TemplateParser.parse(t"<div>Unclosed")
219def test_parse_unexpected_closing_tag():
220 with pytest.raises(ValueError):
221 _ = TemplateParser.parse(t"Unopened</div>")
224def test_self_closing_tags():
225 node = TemplateParser.parse(t"<div/><p></p>")
226 assert node == TFragment(
227 children=(
228 TElement("div"),
229 TElement("p"),
230 )
231 )
234def test_nested_self_closing_tags():
235 node = TemplateParser.parse(t"<div><br><div /><br></div>")
236 assert node == TElement(
237 "div", children=(TElement("br"), TElement("div"), TElement("br"))
238 )
239 node = TemplateParser.parse(t"<div><div /></div>")
240 assert node == TElement("div", children=(TElement("div"),))
243def test_self_closing_tags_unexpected_closing_tag():
244 with pytest.raises(ValueError):
245 _ = TemplateParser.parse(t"<div /></div>")
248def test_self_closing_void_tags_unexpected_closing_tag():
249 with pytest.raises(ValueError):
250 _ = TemplateParser.parse(t"<input /></input>")
253#
254# Attributes
255#
256def test_literal_attrs():
257 node = TemplateParser.parse(
258 t"<a"
259 t" id=example_link" # no quotes allowed without spaces
260 t" autofocus" # bare / boolean
261 t' title=""' # empty attribute
262 t' href="https://example.com" target="_blank"'
263 t">Link</a>"
264 )
265 assert node == TElement(
266 "a",
267 attrs=(
268 TLiteralAttribute("id", "example_link"),
269 TLiteralAttribute("autofocus", None),
270 TLiteralAttribute("title", ""),
271 TLiteralAttribute("href", "https://example.com"),
272 TLiteralAttribute("target", "_blank"),
273 ),
274 children=(TText.literal("Link"),),
275 )
278def test_literal_attr_entities():
279 node = TemplateParser.parse(t'<a title="<">Link</a>')
280 assert node == TElement(
281 "a",
282 attrs=(TLiteralAttribute("title", "<"),),
283 children=(TText.literal("Link"),),
284 )
287def test_literal_attr_order():
288 node = TemplateParser.parse(t'<a title="a" href="b" title="c"></a>')
289 assert isinstance(node, TElement)
290 assert node.attrs == (
291 TLiteralAttribute("title", "a"),
292 TLiteralAttribute("href", "b"),
293 TLiteralAttribute("title", "c"), # dupe IS allowed
294 )
297def test_interpolated_attr():
298 value1 = 42
299 value2 = 99
300 node = TemplateParser.parse(t'<div value1="{value1}" value2={value2} />')
301 assert node == TElement(
302 "div",
303 attrs=(
304 TInterpolatedAttribute("value1", 0),
305 TInterpolatedAttribute("value2", 1),
306 ),
307 children=(),
308 )
311def test_templated_attr():
312 value1 = 42
313 value2 = 99
314 node = TemplateParser.parse(
315 t'<div value1="{value1}-burrito" value2="neato-{value2}-wow" />'
316 )
317 value1_ref = TemplateRef(strings=("", "-burrito"), i_indexes=(0,))
318 value2_ref = TemplateRef(strings=("neato-", "-wow"), i_indexes=(1,))
319 assert node == TElement(
320 "div",
321 attrs=(
322 TTemplatedAttribute("value1", value1_ref),
323 TTemplatedAttribute("value2", value2_ref),
324 ),
325 children=(),
326 )
329def test_spread_attr():
330 spread_attrs = {}
331 node = TemplateParser.parse(t"<div {spread_attrs} />")
332 assert node == TElement(
333 "div",
334 attrs=(TSpreadAttribute(i_index=0),),
335 children=(),
336 )
339def test_templated_attribute_name_error():
340 with pytest.raises(ValueError):
341 attr_name = "some-attr"
342 _ = TemplateParser.parse(t'<div {attr_name}="value" />')
345def test_templated_attribute_name_and_value_error():
346 with pytest.raises(ValueError):
347 attr_name = "some-attr"
348 value = "value"
349 _ = TemplateParser.parse(t'<div {attr_name}="{value}" />')
352def test_adjacent_spread_attrs_error():
353 with pytest.raises(ValueError):
354 attrs1 = {}
355 attrs2 = {}
356 _ = TemplateParser.parse(t"<div {attrs1}{attrs2} />")
359#
360# Comments
361#
362def test_parse_comment():
363 node = TemplateParser.parse(t"<!-- This is a comment -->")
364 assert node == TComment.literal(" This is a comment ")
367def test_parse_comment_interpolation():
368 text = "comment"
369 node = TemplateParser.parse(t"<!-- This is a {text} -->")
370 assert node == TComment(
371 ref=TemplateRef(strings=(" This is a ", " "), i_indexes=(0,))
372 )
375#
376# Doctypes
377#
378def test_parse_doctype():
379 node = TemplateParser.parse(t"<!DOCTYPE html>")
380 assert node == TDocumentType("html")
383def test_parse_doctype_interpolation_error():
384 extra = "SYSTEM"
385 with pytest.raises(ValueError):
386 _ = TemplateParser.parse(t"<!DOCTYPE html {extra}>")
389def test_unsupported_decl_error():
390 with pytest.raises(NotImplementedError):
391 _ = TemplateParser.parse(t"<!doctype-alt html500>") # Unknown declaration
392 with pytest.raises(NotImplementedError):
393 _ = TemplateParser.parse(t"<!doctype>") # missing DTD
396#
397# Components.
398#
399def test_component_element_with_children():
400 def Component(children):
401 return t"{children}"
403 node = TemplateParser.parse(t"<{Component}><div>Hello, World!</div></{Component}>")
404 assert node == TComponent(
405 start_i_index=0,
406 end_i_index=1,
407 children=(TElement("div", children=(TText.literal("Hello, World!"),)),),
408 )
411def test_component_element_self_closing():
412 def Component():
413 pass
415 node = TemplateParser.parse(t"<{Component} />")
416 assert node == TComponent(start_i_index=0)
419def test_component_element_with_closing_tag():
420 def Component():
421 pass
423 node = TemplateParser.parse(t"<{Component}></{Component}>")
424 assert node == TComponent(start_i_index=0, end_i_index=1)
427def test_component_element_special_case_mismatched_closing_tag_still_parses():
428 def Component1():
429 pass
431 def Component2():
432 pass
434 node = TemplateParser.parse(t"<{Component1}></{Component2}>")
435 assert node == TComponent(start_i_index=0, end_i_index=1)
438def test_component_element_invalid_closing_tag():
439 def Component():
440 pass
442 with pytest.raises(ValueError):
443 _ = TemplateParser.parse(t"<{Component}></div>")
446def test_component_element_invalid_opening_tag():
447 def Component():
448 pass
450 with pytest.raises(ValueError):
451 _ = TemplateParser.parse(t"<div></{Component}>")
454def test_adjacent_start_component_tag_error():
455 def Component():
456 pass
458 with pytest.raises(ValueError):
459 _ = TemplateParser.parse(t"<{Component}{Component}></{Component}>")
462def test_adjacent_end_component_tag_error():
463 def Component():
464 pass
466 with pytest.raises(ValueError):
467 _ = TemplateParser.parse(t"<{Component}></{Component}{Component}>")
470def test_placeholder_collision_avoidance():
471 config = make_placeholder_config()
472 # This test is to ensure that our placeholder detection avoids collisions
473 # even with content that might look like a placeholder.
474 tricky = "0"
475 template = Template(
476 f'<div data-tricky="{config.prefix}',
477 Interpolation(tricky, "tricky", None, ""),
478 f'{config.suffix}"></div>',
479 )
480 tnode = TemplateParser.parse(template)
481 value_ref = TemplateRef(strings=(config.prefix, config.suffix), i_indexes=(0,))
482 assert tnode == TElement(
483 "div", attrs=(TTemplatedAttribute(name="data-tricky", value_ref=value_ref),)
484 )