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

1from string.templatelib import Interpolation, Template 

2 

3import pytest 

4 

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) 

19 

20 

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 ) 

46 

47 

48# 

49# Text 

50# 

51def test_parse_empty(): 

52 node = TemplateParser.parse(t"") 

53 assert node == TFragment() 

54 

55 

56def test_parse_text(): 

57 node = TemplateParser.parse(t"Hello, world!") 

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

59 

60 

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

70 

71 

72def test_parse_text_with_entities(): 

73 node = TemplateParser.parse(t"a &lt; b") 

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

75 

76 

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

81 

82 

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

87 

88 

89# 

90# Elements 

91# 

92def test_parse_void_element(): 

93 node = TemplateParser.parse(t"<br>") 

94 assert node == TElement("br") 

95 

96 

97def test_parse_void_element_self_closed(): 

98 node = TemplateParser.parse(t"<br />") 

99 assert node == TElement("br") 

100 

101 

102def test_parse_uppercase_void_element(): 

103 node = TemplateParser.parse(t"<BR>") 

104 assert node == TElement("br") 

105 

106 

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

110 

111 

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 ) 

121 

122 

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 ) 

130 

131 

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 ) 

138 

139 

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 ) 

153 

154 

155def test_parse_text_entities(): 

156 node = TemplateParser.parse(t"<p>&lt;/p&gt;</p>") 

157 assert node == TElement( 

158 "p", 

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

160 ) 

161 

162 

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 ) 

171 

172 

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

176 assert node == TElement( 

177 "script", 

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

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

180 

181 

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 ) 

190 

191 

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

195 assert node == TElement( 

196 "textarea", 

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

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

199 

200 

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 ) 

207 

208 

209def test_parse_mismatched_tags(): 

210 with pytest.raises(ValueError): 

211 _ = TemplateParser.parse(t"<div><span>Mismatched</div></span>") 

212 

213 

214def test_parse_unclosed_tag(): 

215 with pytest.raises(ValueError): 

216 _ = TemplateParser.parse(t"<div>Unclosed") 

217 

218 

219def test_parse_unexpected_closing_tag(): 

220 with pytest.raises(ValueError): 

221 _ = TemplateParser.parse(t"Unopened</div>") 

222 

223 

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 ) 

232 

233 

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

241 

242 

243def test_self_closing_tags_unexpected_closing_tag(): 

244 with pytest.raises(ValueError): 

245 _ = TemplateParser.parse(t"<div /></div>") 

246 

247 

248def test_self_closing_void_tags_unexpected_closing_tag(): 

249 with pytest.raises(ValueError): 

250 _ = TemplateParser.parse(t"<input /></input>") 

251 

252 

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 ) 

276 

277 

278def test_literal_attr_entities(): 

279 node = TemplateParser.parse(t'<a title="&lt;">Link</a>') 

280 assert node == TElement( 

281 "a", 

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

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

284 ) 

285 

286 

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 ) 

295 

296 

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 ) 

309 

310 

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 ) 

327 

328 

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 ) 

337 

338 

339def test_templated_attribute_name_error(): 

340 with pytest.raises(ValueError): 

341 attr_name = "some-attr" 

342 _ = TemplateParser.parse(t'<div {attr_name}="value" />') 

343 

344 

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

350 

351 

352def test_adjacent_spread_attrs_error(): 

353 with pytest.raises(ValueError): 

354 attrs1 = {} 

355 attrs2 = {} 

356 _ = TemplateParser.parse(t"<div {attrs1}{attrs2} />") 

357 

358 

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

365 

366 

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 ) 

373 

374 

375# 

376# Doctypes 

377# 

378def test_parse_doctype(): 

379 node = TemplateParser.parse(t"<!DOCTYPE html>") 

380 assert node == TDocumentType("html") 

381 

382 

383def test_parse_doctype_interpolation_error(): 

384 extra = "SYSTEM" 

385 with pytest.raises(ValueError): 

386 _ = TemplateParser.parse(t"<!DOCTYPE html {extra}>") 

387 

388 

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 

394 

395 

396# 

397# Components. 

398# 

399def test_component_element_with_children(): 

400 def Component(children): 

401 return t"{children}" 

402 

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 ) 

409 

410 

411def test_component_element_self_closing(): 

412 def Component(): 

413 pass 

414 

415 node = TemplateParser.parse(t"<{Component} />") 

416 assert node == TComponent(start_i_index=0) 

417 

418 

419def test_component_element_with_closing_tag(): 

420 def Component(): 

421 pass 

422 

423 node = TemplateParser.parse(t"<{Component}></{Component}>") 

424 assert node == TComponent(start_i_index=0, end_i_index=1) 

425 

426 

427def test_component_element_special_case_mismatched_closing_tag_still_parses(): 

428 def Component1(): 

429 pass 

430 

431 def Component2(): 

432 pass 

433 

434 node = TemplateParser.parse(t"<{Component1}></{Component2}>") 

435 assert node == TComponent(start_i_index=0, end_i_index=1) 

436 

437 

438def test_component_element_invalid_closing_tag(): 

439 def Component(): 

440 pass 

441 

442 with pytest.raises(ValueError): 

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

444 

445 

446def test_component_element_invalid_opening_tag(): 

447 def Component(): 

448 pass 

449 

450 with pytest.raises(ValueError): 

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

452 

453 

454def test_adjacent_start_component_tag_error(): 

455 def Component(): 

456 pass 

457 

458 with pytest.raises(ValueError): 

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

460 

461 

462def test_adjacent_end_component_tag_error(): 

463 def Component(): 

464 pass 

465 

466 with pytest.raises(ValueError): 

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

468 

469 

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 )