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

1from string.templatelib import Interpolation, Template 

2 

3import pytest 

4 

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) 

26 

27 

28def parse_root(t: Template) -> TNode: 

29 """Parse template to ttree and then return the root tnode.""" 

30 return TemplateParser.parse(t).root 

31 

32 

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 ) 

46 

47 

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 ) 

73 

74 

75# 

76# Text 

77# 

78def test_parse_empty(): 

79 node = parse_root(t"") 

80 assert node == TFragment() 

81 

82 

83def test_parse_text(): 

84 node = parse_root(t"Hello, world!") 

85 assert node == TText.literal("Hello, world!") 

86 

87 

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""") 

97 

98 

99def test_parse_text_with_entities(): 

100 node = parse_root(t"a &lt; b") 

101 assert node == TText.literal("a < b") 

102 

103 

104def test_parse_text_with_template_singleton(): 

105 greeting = "Hello, World!" 

106 node = parse_root(t"{greeting}") 

107 assert node == TText(ref=TemplateRef(strings=("", ""))) 

108 

109 

110def test_parse_text_with_template(): 

111 who = "World" 

112 node = parse_root(t"Hello, {who}!") 

113 assert node == TText(ref=TemplateRef(strings=("Hello, ", "!"))) 

114 

115 

116# 

117# Elements 

118# 

119def test_parse_void_element(): 

120 node = parse_root(t"<br>") 

121 assert node == TElement("br") 

122 

123 

124def test_parse_void_element_self_closed(): 

125 node = parse_root(t"<br />") 

126 assert node == TElement("br") 

127 

128 

129def test_parse_uppercase_void_element(): 

130 node = parse_root(t"<BR>") 

131 assert node == TElement("br") 

132 

133 

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!"),)) 

137 

138 

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 ) 

148 

149 

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 ) 

157 

158 

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=("", ""))),)) 

163 

164 

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 ) 

178 

179 

180def test_parse_text_entities(): 

181 node = parse_root(t"<p>&lt;/p&gt;</p>") 

182 assert node == TElement( 

183 "p", 

184 children=(TText.literal("</p>"),), 

185 ) 

186 

187 

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 ) 

194 

195 

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 &amp; b';</script>") 

199 assert node == TElement( 

200 "script", 

201 children=(TText.literal("var x = 'a &amp; b';"),), 

202 ), "Entities SHOULD NOT be evaluated in scripts." 

203 

204 

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 ) 

211 

212 

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 &amp; b';</textarea>") 

216 assert node == TElement( 

217 "textarea", 

218 children=(TText.literal("var x = 'a & b';"),), 

219 ), "Entities SHOULD be evaluated in textarea/title." 

220 

221 

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 ) 

228 

229 

230def test_parse_mismatched_tags(): 

231 with pytest.raises(ParsingError, match="Mismatch"): 

232 _ = parse_root(t"<div><span>Mismatched</div></span>") 

233 

234 

235def test_parse_unclosed_element(): 

236 with pytest.raises(ParsingError, match="unclosed tag <div>"): 

237 _ = parse_root(t"<div>Unclosed") 

238 

239 

240def test_parse_unexpected_closing_tag(): 

241 with pytest.raises(ParsingError, match="Unexpected closing tag"): 

242 _ = parse_root(t"Unopened</div>") 

243 

244 

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 ) 

253 

254 

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"),)) 

262 

263 

264def test_self_closing_tags_unexpected_closing_tag(): 

265 with pytest.raises(ParsingError, match="Unexpected closing tag"): 

266 _ = parse_root(t"<div /></div>") 

267 

268 

269def test_self_closing_void_tags_unexpected_closing_tag(): 

270 with pytest.raises(ParsingError, match="Unexpected closing tag"): 

271 _ = parse_root(t"<input /></input>") 

272 

273 

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 ) 

297 

298 

299def test_literal_attr_entities(): 

300 node = parse_root(t'<a title="&lt;">Link</a>') 

301 assert node == TElement( 

302 "a", 

303 attrs=(TLiteralAttribute("title", "<"),), 

304 children=(TText.literal("Link"),), 

305 ) 

306 

307 

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 ) 

316 

317 

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 ) 

330 

331 

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 ) 

346 

347 

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 ) 

356 

357 

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" />') 

365 

366 

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}" />') 

375 

376 

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} />") 

384 

385 

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 ") 

392 

393 

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 ", " "))) 

398 

399 

400# 

401# Doctypes 

402# 

403def test_parse_doctype(): 

404 node = parse_root(t"<!DOCTYPE html>") 

405 assert node == TDocumentType("html") 

406 

407 

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}>") 

414 

415 

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 

421 

422 

423# 

424# Components. 

425# 

426def test_component_element_with_children(): 

427 def Component(children): 

428 return t"{children}" 

429 

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 ) 

437 

438 

439def test_component_element_self_closing(): 

440 def Component(): 

441 pass 

442 

443 node = parse_root(t"<{Component} />") 

444 assert node == TComponent(start_i_index=0) 

445 

446 

447def test_component_element_with_closing_tag(): 

448 def Component(): 

449 pass 

450 

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 ) 

457 

458 

459def test_component_element_special_case_mismatched_closing_tag_still_parses(): 

460 def Component1(): 

461 pass 

462 

463 def Component2(): 

464 pass 

465 

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 ) 

472 

473 

474def test_component_element_invalid_closing_tag(): 

475 def Component(): 

476 pass 

477 

478 with pytest.raises(ParsingError, match="Mismatched closing tag </div>"): 

479 _ = parse_root(t"<{Component}></div>") 

480 

481 

482def test_component_element_invalid_opening_tag(): 

483 def Component(): 

484 pass 

485 

486 # @NOTE: intentional expression 

487 with pytest.raises(ParsingError, match=r"Mismatched closing tag </\{Component\}>"): 

488 _ = parse_root(t"<div></{Component}>") 

489 

490 

491def test_adjacent_start_component_tag_error(): 

492 def Component(): 

493 pass 

494 

495 with pytest.raises(ParsingError, match="must have exactly one interpolation"): 

496 _ = parse_root(t"<{Component}{Component}></{Component}>") 

497 

498 

499def test_adjacent_end_component_tag_error(): 

500 def Component(): 

501 pass 

502 

503 with pytest.raises(ParsingError, match="must have exactly one interpolation"): 

504 _ = parse_root(t"<{Component}></{Component}{Component}>") 

505 

506 

507def test_unmatched_end_component_tag_error(): 

508 def Component(): 

509 pass 

510 

511 with pytest.raises(ParsingError, match=r"Unexpected closing tag </\{Component\}>"): 

512 _ = TemplateParser.parse(t"</{Component}>") 

513 

514 

515def test_unclosed_component_tag_error(): 

516 def Component(): 

517 pass 

518 

519 with pytest.raises(ParsingError, match=r"unclosed tag <\{Component\}>"): 

520 _ = TemplateParser.parse(t"<{Component}>") 

521 

522 

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 ) 

539 

540 

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() 

550 

551 

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." 

563 

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 ) 

589 

590 

591class TestComponentChildrenSpan: 

592 @pytest.fixture 

593 def Component(self): 

594 def Component(children: Template, **attrs: str) -> Template: 

595 return t"" 

596 

597 return Component 

598 

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 ) 

606 

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 ) 

614 

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 ) 

623 

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 

641 

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 ) 

651 

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 ) 

664 

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 ) 

679 

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 ) 

685 

686 node = parse_root(template) 

687 

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,) 

693 

694 

695class TestSourcePos: 

696 """ 

697 Test that common nodes have a source position translated and set during parsing. 

698 """ 

699 

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 

712 

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 

725 

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 

738 

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 

751 

752 def test_component(self): 

753 def Comp() -> Template: 

754 return t"" 

755 

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 

764 

765 

766class TestSourceInfo: 

767 """ 

768 Test that elements and components have correct entries in the sinfo_table after parsing. 

769 """ 

770 

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 ) 

789 

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 ) 

806 

807 def test_component(self): 

808 def Comp() -> Template: 

809 return t"" 

810 

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 ) 

828 

829 def test_component_self_closed(self): 

830 def Comp() -> Template: 

831 return t"" 

832 

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 ) 

848 

849 def test_multiline_component_spans(self): 

850 def Component(children): 

851 return children 

852 

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 

859 

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) 

863 

864 assert extracted_starttag.strings == ("<", '\n title="literal"\n>') 

865 assert extracted_starttag.values == (Component,) 

866 assert extracted_children.strings == ("child",) 

867 

868 def test_component_spans_with_attribute_entity(self): 

869 def Component(children): 

870 return children 

871 

872 template = t'<{Component} title="&lt;">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 

879 

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) 

883 

884 assert extracted_starttag.strings == ("<", ' title="&lt;">') 

885 assert extracted_starttag.values == (Component,) 

886 assert extracted_children.strings == ("child",) 

887 

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