Coverage for tdom/parser_test.py: 96%
415 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
1from string.templatelib import Interpolation, Template
3import pytest
5from .parser import (
6 AttributeParsingError,
7 ParsingError,
8 TemplateParser,
9 configure_source_tracker,
10)
11from .placeholders import make_placeholder_config
12from .template_utils import PartPosition, TemplateRef, TemplateSpan
13from .tnodes import (
14 TComment,
15 TComponent,
16 TDocumentType,
17 TElement,
18 TFragment,
19 TInterpolatedAttribute,
20 TLiteralAttribute,
21 TNode,
22 TSpreadAttribute,
23 TTemplatedAttribute,
24 TText,
25)
28def parse_root(t: Template) -> TNode:
29 """Parse template to ttree and then return the root tnode."""
30 return TemplateParser.parse(t).root
33def literal_template_span(template: Template, text: str) -> TemplateSpan:
34 """Return the span of a unique literal substring in a template."""
35 matches = [
36 (index, string.index(text))
37 for index, string in enumerate(template.strings)
38 if text in string
39 ]
40 assert len(matches) == 1
41 string_index, offset = matches[0]
42 return TemplateSpan(
43 start=PartPosition(string_index, offset),
44 stop=PartPosition(string_index, offset + len(text)),
45 )
48def test_parse_mixed_literal_content():
49 node = parse_root(
50 t"<!DOCTYPE html>"
51 t"<!-- Comment -->"
52 t'<div class="container">'
53 t"Hello, <br class='funky' />world <!-- neato -->!"
54 t"</div>"
55 )
56 assert node == TFragment(
57 children=(
58 TDocumentType("html"),
59 TComment.literal(" Comment "),
60 TElement(
61 "div",
62 attrs=(TLiteralAttribute("class", "container"),),
63 children=(
64 TText.literal("Hello, "),
65 TElement("br", attrs=(TLiteralAttribute("class", "funky"),)),
66 TText.literal("world "),
67 TComment.literal(" neato "),
68 TText.literal("!"),
69 ),
70 ),
71 )
72 )
75#
76# Text
77#
78def test_parse_empty():
79 node = parse_root(t"")
80 assert node == TFragment()
83def test_parse_text():
84 node = parse_root(t"Hello, world!")
85 assert node == TText.literal("Hello, world!")
88def test_parse_text_multiline():
89 node = parse_root(t"""Hello, world!
90 Hello, moon!
91Hello, sun!
92""")
93 assert node == TText.literal("""Hello, world!
94 Hello, moon!
95Hello, sun!
96""")
99def test_parse_text_with_entities():
100 node = parse_root(t"a < b")
101 assert node == TText.literal("a < b")
104def test_parse_text_with_template_singleton():
105 greeting = "Hello, World!"
106 node = parse_root(t"{greeting}")
107 assert node == TText(ref=TemplateRef(strings=("", "")))
110def test_parse_text_with_template():
111 who = "World"
112 node = parse_root(t"Hello, {who}!")
113 assert node == TText(ref=TemplateRef(strings=("Hello, ", "!")))
116#
117# Elements
118#
119def test_parse_void_element():
120 node = parse_root(t"<br>")
121 assert node == TElement("br")
124def test_parse_void_element_self_closed():
125 node = parse_root(t"<br />")
126 assert node == TElement("br")
129def test_parse_uppercase_void_element():
130 node = parse_root(t"<BR>")
131 assert node == TElement("br")
134def test_parse_standard_element_with_text():
135 node = parse_root(t"<div>Hello, world!</div>")
136 assert node == TElement("div", children=(TText.literal("Hello, world!"),))
139def test_parse_nested_elements():
140 node = parse_root(t"<div><span>Nested</span> content</div>")
141 assert node == TElement(
142 "div",
143 children=(
144 TElement("span", children=(TText.literal("Nested"),)),
145 TText.literal(" content"),
146 ),
147 )
150def test_parse_element_with_template():
151 who = "World"
152 node = parse_root(t"<div>Hello, {who}!</div>")
153 assert node == TElement(
154 "div",
155 children=(TText(ref=TemplateRef(strings=("Hello, ", "!"))),),
156 )
159def test_parse_element_with_template_singleton():
160 greeting = "Hello, World!"
161 node = parse_root(t"<div>{greeting}</div>")
162 assert node == TElement("div", children=(TText(ref=TemplateRef(strings=("", ""))),))
165def test_parse_multiple_voids():
166 node = parse_root(t"<br><hr><hr /><hr /><br /><br><br>")
167 assert node == TFragment(
168 children=(
169 TElement("br"),
170 TElement("hr"),
171 TElement("hr"),
172 TElement("hr"),
173 TElement("br"),
174 TElement("br"),
175 TElement("br"),
176 )
177 )
180def test_parse_text_entities():
181 node = parse_root(t"<p></p></p>")
182 assert node == TElement(
183 "p",
184 children=(TText.literal("</p>"),),
185 )
188def test_parse_script_tag_content():
189 node = parse_root(t"<script>if (a < b && c > d) { alert('wow'); } </script>")
190 assert node == TElement(
191 "script",
192 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
193 )
196def test_parse_script_with_entities():
197 # The <script> tag (and <style>) tag uses the CDATA content model.
198 node = parse_root(t"<script>var x = 'a & b';</script>")
199 assert node == TElement(
200 "script",
201 children=(TText.literal("var x = 'a & b';"),),
202 ), "Entities SHOULD NOT be evaluated in scripts."
205def test_parse_textarea_tag_content():
206 node = parse_root(t"<textarea>if (a < b && c > d) { alert('wow'); } </textarea>")
207 assert node == TElement(
208 "textarea",
209 children=(TText.literal("if (a < b && c > d) { alert('wow'); }"),),
210 )
213def test_parse_textarea_with_entities():
214 # The <textarea> (and <title>) tag uses the RCDATA content model.
215 node = parse_root(t"<textarea>var x = 'a & b';</textarea>")
216 assert node == TElement(
217 "textarea",
218 children=(TText.literal("var x = 'a & b';"),),
219 ), "Entities SHOULD be evaluated in textarea/title."
222def test_parse_title_unusual():
223 node = parse_root(t"<title>My & Awesome <Site></title>")
224 assert node == TElement(
225 "title",
226 children=(TText.literal("My & Awesome <Site>"),),
227 )
230def test_parse_mismatched_tags():
231 with pytest.raises(ParsingError, match="Mismatch"):
232 _ = parse_root(t"<div><span>Mismatched</div></span>")
235def test_parse_unclosed_element():
236 with pytest.raises(ParsingError, match="unclosed tag <div>"):
237 _ = parse_root(t"<div>Unclosed")
240def test_parse_unexpected_closing_tag():
241 with pytest.raises(ParsingError, match="Unexpected closing tag"):
242 _ = parse_root(t"Unopened</div>")
245def test_self_closing_tags():
246 node = parse_root(t"<div/><p></p>")
247 assert node == TFragment(
248 children=(
249 TElement("div"),
250 TElement("p"),
251 )
252 )
255def test_nested_self_closing_tags():
256 node = parse_root(t"<div><br><div /><br></div>")
257 assert node == TElement(
258 "div", children=(TElement("br"), TElement("div"), TElement("br"))
259 )
260 node = parse_root(t"<div><div /></div>")
261 assert node == TElement("div", children=(TElement("div"),))
264def test_self_closing_tags_unexpected_closing_tag():
265 with pytest.raises(ParsingError, match="Unexpected closing tag"):
266 _ = parse_root(t"<div /></div>")
269def test_self_closing_void_tags_unexpected_closing_tag():
270 with pytest.raises(ParsingError, match="Unexpected closing tag"):
271 _ = parse_root(t"<input /></input>")
274#
275# Attributes
276#
277def test_literal_attrs():
278 node = parse_root(
279 t"<a"
280 t" id=example_link" # no quotes allowed without spaces
281 t" autofocus" # bare / boolean
282 t' title=""' # empty attribute
283 t' href="https://example.com" target="_blank"'
284 t">Link</a>"
285 )
286 assert node == TElement(
287 "a",
288 attrs=(
289 TLiteralAttribute("id", "example_link"),
290 TLiteralAttribute("autofocus", None),
291 TLiteralAttribute("title", ""),
292 TLiteralAttribute("href", "https://example.com"),
293 TLiteralAttribute("target", "_blank"),
294 ),
295 children=(TText.literal("Link"),),
296 )
299def test_literal_attr_entities():
300 node = parse_root(t'<a title="<">Link</a>')
301 assert node == TElement(
302 "a",
303 attrs=(TLiteralAttribute("title", "<"),),
304 children=(TText.literal("Link"),),
305 )
308def test_literal_attr_order():
309 node = parse_root(t'<a title="a" href="b" title="c"></a>')
310 assert isinstance(node, TElement)
311 assert node.attrs == (
312 TLiteralAttribute("title", "a"),
313 TLiteralAttribute("href", "b"),
314 TLiteralAttribute("title", "c"), # dupe IS allowed
315 )
318def test_interpolated_attr():
319 value1 = 42
320 value2 = 99
321 node = parse_root(t'<div value1="{value1}" value2={value2} />')
322 assert node == TElement(
323 "div",
324 attrs=(
325 TInterpolatedAttribute("value1", 0),
326 TInterpolatedAttribute("value2", 1),
327 ),
328 children=(),
329 )
332def test_templated_attr():
333 value1 = 42
334 value2 = 99
335 node = parse_root(t'<div value1="{value1}-burrito" value2="neato-{value2}-wow" />')
336 value1_ref = TemplateRef(strings=("", "-burrito"))
337 value2_ref = TemplateRef(strings=("neato-", "-wow"), i_start=1)
338 assert node == TElement(
339 "div",
340 attrs=(
341 TTemplatedAttribute("value1", value1_ref),
342 TTemplatedAttribute("value2", value2_ref),
343 ),
344 children=(),
345 )
348def test_spread_attr():
349 spread_attrs = {}
350 node = parse_root(t"<div {spread_attrs} />")
351 assert node == TElement(
352 "div",
353 attrs=(TSpreadAttribute(i_index=0),),
354 children=(),
355 )
358def test_templated_attribute_name_error():
359 attr_name = "some-attr"
360 with pytest.raises(
361 AttributeParsingError,
362 match="cannot contain interpolations if the value is also interpolated",
363 ):
364 _ = parse_root(t'<div {attr_name}="value" />')
367def test_templated_attribute_name_and_value_error():
368 attr_name = "some-attr"
369 value = "value"
370 with pytest.raises(
371 AttributeParsingError,
372 match="cannot contain interpolations if the value is also interpolated",
373 ):
374 _ = parse_root(t'<div {attr_name}="{value}" />')
377def test_adjacent_spread_attrs_error():
378 attrs1 = {}
379 attrs2 = {}
380 with pytest.raises(
381 AttributeParsingError, match="must have exactly one interpolation in the name"
382 ):
383 _ = parse_root(t"<div {attrs1}{attrs2} />")
386#
387# Comments
388#
389def test_parse_comment():
390 node = parse_root(t"<!-- This is a comment -->")
391 assert node == TComment.literal(" This is a comment ")
394def test_parse_comment_interpolation():
395 text = "comment"
396 node = parse_root(t"<!-- This is a {text} -->")
397 assert node == TComment(ref=TemplateRef(strings=(" This is a ", " ")))
400#
401# Doctypes
402#
403def test_parse_doctype():
404 node = parse_root(t"<!DOCTYPE html>")
405 assert node == TDocumentType("html")
408def test_parse_doctype_interpolation_error():
409 extra = "SYSTEM"
410 with pytest.raises(
411 ParsingError, match="Interpolations are not allowed in declarations"
412 ):
413 _ = parse_root(t"<!DOCTYPE html {extra}>")
416def test_unsupported_decl_error():
417 with pytest.raises(ParsingError, match="Only well formed DOCTYPE declarations"):
418 _ = parse_root(t"<!doctype-alt html500>") # Unknown declaration
419 with pytest.raises(ParsingError, match="Only well formed DOCTYPE declarations"):
420 _ = parse_root(t"<!doctype>") # missing DTD
423#
424# Components.
425#
426def test_component_element_with_children():
427 def Component(children):
428 return t"{children}"
430 template = t"<{Component}><div>Hello, World!</div></{Component}>"
431 node = parse_root(template)
432 assert node == TComponent(
433 start_i_index=0,
434 end_i_index=1,
435 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
436 )
439def test_component_element_self_closing():
440 def Component():
441 pass
443 node = parse_root(t"<{Component} />")
444 assert node == TComponent(start_i_index=0)
447def test_component_element_with_closing_tag():
448 def Component():
449 pass
451 node = parse_root(t"<{Component}></{Component}>")
452 assert node == TComponent(
453 start_i_index=0,
454 end_i_index=1,
455 children_span=TemplateSpan(PartPosition(1, 1), PartPosition(1, 1)),
456 )
459def test_component_element_special_case_mismatched_closing_tag_still_parses():
460 def Component1():
461 pass
463 def Component2():
464 pass
466 node = parse_root(t"<{Component1}></{Component2}>")
467 assert node == TComponent(
468 start_i_index=0,
469 end_i_index=1,
470 children_span=TemplateSpan(PartPosition(1, 1), PartPosition(1, 1)),
471 )
474def test_component_element_invalid_closing_tag():
475 def Component():
476 pass
478 with pytest.raises(ParsingError, match="Mismatched closing tag </div>"):
479 _ = parse_root(t"<{Component}></div>")
482def test_component_element_invalid_opening_tag():
483 def Component():
484 pass
486 # @NOTE: intentional expression
487 with pytest.raises(ParsingError, match=r"Mismatched closing tag </\{Component\}>"):
488 _ = parse_root(t"<div></{Component}>")
491def test_adjacent_start_component_tag_error():
492 def Component():
493 pass
495 with pytest.raises(ParsingError, match="must have exactly one interpolation"):
496 _ = parse_root(t"<{Component}{Component}></{Component}>")
499def test_adjacent_end_component_tag_error():
500 def Component():
501 pass
503 with pytest.raises(ParsingError, match="must have exactly one interpolation"):
504 _ = parse_root(t"<{Component}></{Component}{Component}>")
507def test_unmatched_end_component_tag_error():
508 def Component():
509 pass
511 with pytest.raises(ParsingError, match=r"Unexpected closing tag </\{Component\}>"):
512 _ = TemplateParser.parse(t"</{Component}>")
515def test_unclosed_component_tag_error():
516 def Component():
517 pass
519 with pytest.raises(ParsingError, match=r"unclosed tag <\{Component\}>"):
520 _ = TemplateParser.parse(t"<{Component}>")
523def test_placeholder_collision_avoidance():
524 config = make_placeholder_config()
525 # This test is to ensure that our placeholder detection avoids collisions
526 # even with content that might look like a placeholder.
527 tricky = "0"
528 template = Template(
529 f'<div data-tricky="{config.prefix}',
530 Interpolation(tricky, "tricky", None, ""),
531 f'{config.suffix}"></div>',
532 )
533 tnode = parse_root(template)
534 value_ref = TemplateRef(strings=(config.prefix, config.suffix))
535 assert tnode == TElement(
536 "div",
537 attrs=(TTemplatedAttribute(name="data-tricky", value_ref=value_ref),),
538 )
541def test_unresolved_placeholder():
542 t = t"<div>{0}<span>{1}</span>{2}</div>"
543 tp = TemplateParser()
544 tp.feed_template(t)
545 # This would be a bug in the parser so we have to fabricate
546 # this error manually.
547 tp.get_source().placeholders.add_placeholder(3)
548 with pytest.raises(ParsingError, match="Some placeholders were never resolved"):
549 tp.close()
552class TestSourceTracker:
553 @pytest.mark.parametrize("t", (t"", t"simple"))
554 def test_only_string(self, t: Template):
555 st = configure_source_tracker(t)
556 itr = iter(st)
557 part = next(itr)
558 assert isinstance(part, str) and part == t.strings[0]
559 assert itr.index == 0
560 with pytest.raises(StopIteration):
561 _ = next(itr)
562 assert itr.index == 0, "Still at 0."
564 def test_iter(self):
565 t = t"<div>{0}</div>"
566 st = configure_source_tracker(t)
567 itr = iter(st)
568 parts = []
569 parts.append(next(itr))
570 assert itr.index == 0
571 assert not itr.has_placeholders()
572 assert parts[0] == t.strings[0], "String parts pass through."
573 parts.append(next(itr))
574 assert itr.index == 1
575 assert itr.has_placeholders(), "Placeholder was addeded."
576 tref_find = st.find_placeholders(parts[1])
577 assert itr.has_placeholders() and tref_find.i_start == 0
578 tref_removed = st.remove_placeholders(parts[1])
579 assert not itr.has_placeholders() and tref_removed.i_start == 0, (
580 "Placeholder removed."
581 )
582 parts.append(next(itr))
583 assert itr.index == 2
584 with pytest.raises(StopIteration):
585 next(itr)
586 assert itr.index == 2, (
587 "Once the iter is exhausted the index remains at the last element."
588 )
591class TestComponentChildrenSpan:
592 @pytest.fixture
593 def Component(self):
594 def Component(children: Template, **attrs: str) -> Template:
595 return t""
597 return Component
599 def test_extract_no_content(self, Component):
600 node = parse_root(t"<{Component}></{Component}>")
601 assert node == TComponent(
602 start_i_index=0,
603 end_i_index=1,
604 children_span=TemplateSpan(PartPosition(1, 1), PartPosition(1, 1)),
605 )
607 def test_extract_startend(self, Component):
608 node = parse_root(t"<{Component} />")
609 assert node == TComponent(
610 start_i_index=0,
611 end_i_index=None,
612 children_span=None,
613 )
615 def test_extract(self, Component):
616 template = t"<{Component}><div>Hello, World!</div></{Component}>"
617 node = parse_root(template)
618 assert node == TComponent(
619 start_i_index=0,
620 end_i_index=1,
621 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
622 )
624 def test_extract_with_attr_interpolation(self, Component):
625 # Unquoted ...
626 template = t"<{Component} title={'Skip over this.'}><div>Hello, World!</div></{Component}>"
627 node = parse_root(template)
628 assert node == TComponent(
629 start_i_index=0,
630 end_i_index=2,
631 attrs=(TInterpolatedAttribute(name="title", value_i_index=1),),
632 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
633 )
634 # Quoted...
635 node2 = parse_root(
636 t'<{Component} title="{"Skip over this."}"><div>Hello, World!</div></{Component}>'
637 )
638 assert isinstance(node2, TComponent)
639 assert node2.attrs == (TInterpolatedAttribute(name="title", value_i_index=1),)
640 assert node2.children_span is not None
642 def test_extract_with_literal_attr_gt_char(self, Component):
643 template = t'<{Component} title="1 > 0"><div>Hello, World!</div></{Component}>'
644 node = parse_root(template)
645 assert node == TComponent(
646 start_i_index=0,
647 end_i_index=1,
648 attrs=(TLiteralAttribute("title", "1 > 0"),),
649 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
650 )
652 def test_extract_with_interpolated_attr_literal_attr_gt_char(self, Component):
653 template = t'<{Component} id={"simple"} title="1 > 0"><div>Hello, World!</div></{Component}>'
654 node = parse_root(template)
655 assert node == TComponent(
656 start_i_index=0,
657 end_i_index=2,
658 attrs=(
659 TInterpolatedAttribute(name="id", value_i_index=1),
660 TLiteralAttribute("title", "1 > 0"),
661 ),
662 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
663 )
665 def test_extract_with_templated_attr_gt_char(self, Component):
666 template = t'<{Component} id="{"header"}_{"container"}" title="1 > 0"><div>Hello, World!</div></{Component}>'
667 node = parse_root(template)
668 assert node == TComponent(
669 start_i_index=0,
670 end_i_index=3,
671 attrs=(
672 TTemplatedAttribute(
673 "id", TemplateRef(strings=("", "_", ""), i_start=1)
674 ),
675 TLiteralAttribute("title", "1 > 0"),
676 ),
677 children_span=literal_template_span(template, "<div>Hello, World!</div>"),
678 )
680 def test_extract_with_interpolated_children_after_attribute(self, Component):
681 child = "child"
682 template = (
683 t"<{Component} title={'attribute'}>before {child} after</{Component}>"
684 )
686 node = parse_root(template)
688 assert isinstance(node, TComponent)
689 assert node.children_span is not None
690 children = node.children_span.extract(template)
691 assert children.strings == ("before ", " after")
692 assert children.values == (child,)
695class TestSourcePos:
696 """
697 Test that common nodes have a source position translated and set during parsing.
698 """
700 @pytest.mark.parametrize(
701 ("t", "part_pos"),
702 (
703 (t"ABC<div></div>", PartPosition(0, offset=len("ABC"))),
704 (t"{' '}<div></div>", PartPosition(1, offset=0)),
705 ),
706 )
707 def test_el(self, t: Template, part_pos: PartPosition):
708 root = parse_root(t)
709 assert isinstance(root, TFragment)
710 node = root.children[1]
711 assert isinstance(node, TElement) and node.source_pos == part_pos
713 @pytest.mark.parametrize(
714 ("t", "part_pos"),
715 (
716 (t"<div></div>ABC", PartPosition(0, offset=len("<div></div>"))),
717 (t"<div>{' '}</div>ABC", PartPosition(1, offset=len("</div>"))),
718 ),
719 )
720 def test_text(self, t: Template, part_pos: PartPosition):
721 root = parse_root(t)
722 assert isinstance(root, TFragment)
723 node = root.children[1]
724 assert isinstance(node, TText) and node.source_pos == part_pos
726 @pytest.mark.parametrize(
727 ("t", "part_pos"),
728 (
729 (t" <!doctype html>", PartPosition(0, offset=2)),
730 (t"{' '}<!doctype html>", PartPosition(1, 0)),
731 ),
732 )
733 def test_doctype(self, t: Template, part_pos: PartPosition):
734 root = parse_root(t)
735 assert isinstance(root, TFragment)
736 node = root.children[1]
737 assert isinstance(node, TDocumentType) and node.source_pos == part_pos
739 @pytest.mark.parametrize(
740 ("t", "part_pos"),
741 (
742 (t" <!--comment-->", PartPosition(0, offset=2)),
743 (t"<div>{'ABC'}</div><!--comment-->", PartPosition(1, len("</div>"))),
744 ),
745 )
746 def test_comment(self, t: Template, part_pos: PartPosition):
747 root = parse_root(t)
748 assert isinstance(root, TFragment)
749 node = root.children[1]
750 assert isinstance(node, TComment) and node.source_pos == part_pos
752 def test_component(self):
753 def Comp() -> Template:
754 return t""
756 for t, part_pos in (
757 (t" <{Comp} />", PartPosition(0, offset=len(" "))),
758 (t" {'ABC'}DEF<{Comp} />", PartPosition(1, offset=len("DEF"))),
759 ):
760 root = parse_root(t)
761 assert isinstance(root, TFragment)
762 node = root.children[1]
763 assert isinstance(node, TComponent) and node.source_pos == part_pos
766class TestSourceInfo:
767 """
768 Test that elements and components have correct entries in the sinfo_table after parsing.
769 """
771 def test_el(self):
772 ttree = TemplateParser.parse(t"<div></div>")
773 sinfo_table = ttree.unpack_sinfo_table()
774 node = ttree.root
775 assert (
776 isinstance(node, TElement)
777 and sinfo_table
778 and node.source_pos is not None
779 and node.source_pos in sinfo_table
780 )
781 sinfo = sinfo_table[node.source_pos]
782 assert sinfo.startend == False
783 assert sinfo.starttag_pos == PartPosition(
784 0, 0
785 ) and sinfo.endtag_pos == PartPosition(0, len("<div>"))
786 assert sinfo.starttag_span == TemplateSpan(
787 PartPosition(0, 0), PartPosition(0, len("<div>"))
788 )
790 def test_el_self_closed(self):
791 ttree = TemplateParser.parse(t"<div />")
792 sinfo_table = ttree.unpack_sinfo_table()
793 node = ttree.root
794 assert (
795 isinstance(node, TElement)
796 and sinfo_table
797 and node.source_pos is not None
798 and node.source_pos in sinfo_table
799 )
800 sinfo = sinfo_table[node.source_pos]
801 assert sinfo.startend == True
802 assert sinfo.starttag_pos == PartPosition(0, 0) and sinfo.endtag_pos is None
803 assert sinfo.starttag_span == TemplateSpan(
804 PartPosition(0, 0), PartPosition(0, len("<div />"))
805 )
807 def test_component(self):
808 def Comp() -> Template:
809 return t""
811 ttree = TemplateParser.parse(t"<{Comp}></{Comp}>")
812 sinfo_table = ttree.unpack_sinfo_table()
813 node = ttree.root
814 assert (
815 isinstance(node, TComponent)
816 and sinfo_table
817 and node.source_pos is not None
818 and node.source_pos in sinfo_table
819 )
820 sinfo = sinfo_table[node.source_pos]
821 assert sinfo.startend == False
822 assert sinfo.starttag_pos == PartPosition(
823 0, 0
824 ) and sinfo.endtag_pos == PartPosition(1, len(">"))
825 assert sinfo.starttag_span == TemplateSpan(
826 PartPosition(0, 0), PartPosition(1, len(">"))
827 )
829 def test_component_self_closed(self):
830 def Comp() -> Template:
831 return t""
833 ttree = TemplateParser.parse(t"<{Comp} />")
834 sinfo_table = ttree.unpack_sinfo_table()
835 node = ttree.root
836 assert (
837 isinstance(node, TComponent)
838 and sinfo_table
839 and node.source_pos is not None
840 and node.source_pos in sinfo_table
841 )
842 sinfo = sinfo_table[node.source_pos]
843 assert sinfo.startend == True
844 assert sinfo.starttag_pos == PartPosition(0, 0) and sinfo.endtag_pos is None
845 assert sinfo.starttag_span == TemplateSpan(
846 PartPosition(0, 0), PartPosition(1, len(" />"))
847 )
849 def test_multiline_component_spans(self):
850 def Component(children):
851 return children
853 template = t'<{Component}\n title="literal"\n>child</{Component}>'
854 ttree = TemplateParser.parse(template)
855 node = ttree.root
856 assert isinstance(node, TComponent)
857 assert node.children_span is not None
858 assert node.source_pos is not None
860 sinfo = ttree.unpack_sinfo_table()[node.source_pos]
861 extracted_starttag = sinfo.starttag_span.extract(template)
862 extracted_children = node.children_span.extract(template)
864 assert extracted_starttag.strings == ("<", '\n title="literal"\n>')
865 assert extracted_starttag.values == (Component,)
866 assert extracted_children.strings == ("child",)
868 def test_component_spans_with_attribute_entity(self):
869 def Component(children):
870 return children
872 template = t'<{Component} title="<">child</{Component}>'
873 ttree = TemplateParser.parse(template)
874 node = ttree.root
875 assert isinstance(node, TComponent)
876 assert node.attrs == (TLiteralAttribute("title", "<"),)
877 assert node.children_span is not None
878 assert node.source_pos is not None
880 sinfo = ttree.unpack_sinfo_table()[node.source_pos]
881 extracted_starttag = sinfo.starttag_span.extract(template)
882 extracted_children = node.children_span.extract(template)
884 assert extracted_starttag.strings == ("<", ' title="<">')
885 assert extracted_starttag.values == (Component,)
886 assert extracted_children.strings == ("child",)
888 def test_empty_sinfo_table(self):
889 ttree = TemplateParser.parse(t"<!doctype html>ABC")
890 sinfo_table = ttree.unpack_sinfo_table()
891 assert not sinfo_table