Coverage for tests/test_references.py: 100.00%

104 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-10 16:32 +0100

1"""Tests for the references module.""" 

2 

3from __future__ import annotations 

4 

5from textwrap import dedent 

6from typing import TYPE_CHECKING 

7 

8import markdown 

9import pytest 

10 

11from mkdocs_autorefs.plugin import AutorefsPlugin 

12from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Mapping 

16 

17 

18@pytest.mark.parametrize( 

19 ("current_url", "to_url", "href_url"), 

20 [ 

21 ("a/", "a#b", "#b"), 

22 ("a/", "a/b#c", "b#c"), 

23 ("a/b/", "a/b#c", "#c"), 

24 ("a/b/", "a/c#d", "../c#d"), 

25 ("a/b/", "a#c", "..#c"), 

26 ("a/b/c/", "d#e", "../../../d#e"), 

27 ("a/b/", "c/d/#e", "../../c/d/#e"), 

28 ("a/index.html", "a/index.html#b", "#b"), 

29 ("a/index.html", "a/b.html#c", "b.html#c"), 

30 ("a/b.html", "a/b.html#c", "#c"), 

31 ("a/b.html", "a/c.html#d", "c.html#d"), 

32 ("a/b.html", "a/index.html#c", "index.html#c"), 

33 ("a/b/c.html", "d.html#e", "../../d.html#e"), 

34 ("a/b.html", "c/d.html#e", "../c/d.html#e"), 

35 ("a/b/index.html", "a/b/c/d.html#e", "c/d.html#e"), 

36 ("", "#x", "#x"), 

37 ("a/", "#x", "../#x"), 

38 ("a/b.html", "#x", "../#x"), 

39 ("", "a/#x", "a/#x"), 

40 ("", "a/b.html#x", "a/b.html#x"), 

41 ], 

42) 

43def test_relative_url(current_url: str, to_url: str, href_url: str) -> None: 

44 """Compute relative URLs correctly.""" 

45 assert relative_url(current_url, to_url) == href_url 

46 

47 

48def run_references_test( 

49 url_map: dict[str, str], 

50 source: str, 

51 output: str, 

52 unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None, 

53 from_url: str = "page.html", 

54 extensions: Mapping = {}, 

55) -> None: 

56 """Help running tests about references. 

57 

58 Arguments: 

59 url_map: The URL mapping. 

60 source: The source text. 

61 output: The expected output. 

62 unmapped: The expected unmapped list. 

63 from_url: The source page URL. 

64 """ 

65 md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions) 

66 content = md.convert(source) 

67 

68 def url_mapper(identifier: str) -> str: 

69 return relative_url(from_url, url_map[identifier]) 

70 

71 actual_output, actual_unmapped = fix_refs(content, url_mapper) 

72 assert actual_output == output 

73 assert actual_unmapped == (unmapped or []) 

74 

75 

76def test_reference_implicit() -> None: 

77 """Check implicit references (identifier only).""" 

78 run_references_test( 

79 url_map={"Foo": "foo.html#Foo"}, 

80 source="This [Foo][].", 

81 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo">Foo</a>.</p>', 

82 ) 

83 

84 

85def test_reference_explicit_with_markdown_text() -> None: 

86 """Check explicit references with Markdown formatting.""" 

87 run_references_test( 

88 url_map={"Foo": "foo.html#Foo"}, 

89 source="This [**Foo**][Foo].", 

90 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><strong>Foo</strong></a>.</p>', 

91 ) 

92 

93 

94def test_reference_implicit_with_code() -> None: 

95 """Check implicit references (identifier only, wrapped in backticks).""" 

96 run_references_test( 

97 url_map={"Foo": "foo.html#Foo"}, 

98 source="This [`Foo`][].", 

99 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>Foo</code></a>.</p>', 

100 ) 

101 

102 

103def test_reference_implicit_with_code_inlinehilite_plain() -> None: 

104 """Check implicit references (identifier in backticks, wrapped by inlinehilite).""" 

105 run_references_test( 

106 extensions={"pymdownx.inlinehilite": {}}, 

107 url_map={"pathlib.Path": "pathlib.html#Path"}, 

108 source="This [`pathlib.Path`][].", 

109 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code>pathlib.Path</code></a>.</p>', 

110 ) 

111 

112 

113def test_reference_implicit_with_code_inlinehilite_python() -> None: 

114 """Check implicit references (identifier in backticks, syntax-highlighted by inlinehilite).""" 

115 run_references_test( 

116 extensions={"pymdownx.inlinehilite": {"style_plain_text": "python"}, "pymdownx.highlight": {}}, 

117 url_map={"pathlib.Path": "pathlib.html#Path"}, 

118 source="This [`pathlib.Path`][].", 

119 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code class="highlight">pathlib.Path</code></a>.</p>', 

120 ) 

121 

122 

123def test_reference_with_punctuation() -> None: 

124 """Check references with punctuation.""" 

125 run_references_test( 

126 url_map={'Foo&"bar': 'foo.html#Foo&"bar'}, 

127 source='This [Foo&"bar][].', 

128 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo&amp;&quot;bar">Foo&amp;"bar</a>.</p>', 

129 ) 

130 

131 

132def test_reference_to_relative_path() -> None: 

133 """Check references from a page at a nested path.""" 

134 run_references_test( 

135 from_url="sub/sub/page.html", 

136 url_map={"zz": "foo.html#zz"}, 

137 source="This [zz][].", 

138 output='<p>This <a class="autorefs autorefs-internal" href="../../foo.html#zz">zz</a>.</p>', 

139 ) 

140 

141 

142def test_multiline_links() -> None: 

143 """Check that links with multiline text are recognized.""" 

144 run_references_test( 

145 url_map={"foo-bar": "foo.html#bar"}, 

146 source="This [Foo\nbar][foo-bar].", 

147 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo\nbar</a>.</p>', 

148 ) 

149 

150 

151def test_no_reference_with_space() -> None: 

152 """Check that references with spaces are fixed.""" 

153 run_references_test( 

154 url_map={"Foo bar": "foo.html#bar"}, 

155 source="This [Foo bar][].", 

156 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo bar</a>.</p>', 

157 ) 

158 

159 

160def test_no_reference_inside_markdown() -> None: 

161 """Check that references inside code are not fixed.""" 

162 run_references_test( 

163 url_map={"Foo": "foo.html#Foo"}, 

164 source="This `[Foo][]`.", 

165 output="<p>This <code>[Foo][]</code>.</p>", 

166 ) 

167 

168 

169def test_missing_reference() -> None: 

170 """Check that implicit references are correctly seen as unmapped.""" 

171 run_references_test( 

172 url_map={"NotFoo": "foo.html#NotFoo"}, 

173 source="[Foo][]", 

174 output="<p>[Foo][]</p>", 

175 unmapped=[("Foo", None)], 

176 ) 

177 

178 

179def test_missing_reference_with_markdown_text() -> None: 

180 """Check unmapped explicit references.""" 

181 run_references_test( 

182 url_map={"NotFoo": "foo.html#NotFoo"}, 

183 source="[`Foo`][Foo]", 

184 output="<p>[<code>Foo</code>][]</p>", 

185 unmapped=[("Foo", None)], 

186 ) 

187 

188 

189def test_missing_reference_with_markdown_id() -> None: 

190 """Check unmapped explicit references with Markdown in the identifier.""" 

191 run_references_test( 

192 url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"}, 

193 source="[Foo][*NotFoo*]", 

194 output="<p>[Foo][*NotFoo*]</p>", 

195 unmapped=[("*NotFoo*", None)], 

196 ) 

197 

198 

199def test_missing_reference_with_markdown_implicit() -> None: 

200 """Check that implicit references are not fixed when the identifier is not the exact one.""" 

201 run_references_test( 

202 url_map={"Foo-bar": "foo.html#Foo-bar"}, 

203 source="[*Foo-bar*][] and [`Foo`-bar][]", 

204 output="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][`Foo`-bar]</p>", 

205 unmapped=[("*Foo-bar*", None), ("`Foo`-bar", None)], 

206 ) 

207 

208 

209def test_reference_with_markup() -> None: 

210 """Check that references with markup are resolved (and need escaping to prevent rendering).""" 

211 run_references_test( 

212 url_map={"*a b*": "foo.html#Foo"}, 

213 source="This [*a b*][].", 

214 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><em>a b</em></a>.</p>', 

215 ) 

216 run_references_test( 

217 url_map={"*a/b*": "foo.html#Foo"}, 

218 source="This [`*a/b*`][].", 

219 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>*a/b*</code></a>.</p>', 

220 ) 

221 

222 

223def test_legacy_custom_required_reference() -> None: 

224 """Check that external HTML-based references are expanded or reported missing.""" 

225 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

226 run_references_test( 

227 url_map={"ok": "ok.html#ok"}, 

228 source="<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>", 

229 output='<p>[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', 

230 unmapped=[("bar", None)], 

231 ) 

232 

233 

234def test_custom_required_reference() -> None: 

235 """Check that external HTML-based references are expanded or reported missing.""" 

236 run_references_test( 

237 url_map={"ok": "ok.html#ok"}, 

238 source="<autoref identifier=bar>foo</autoref> <autoref identifier=ok>ok</autoref>", 

239 output='<p>[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', 

240 unmapped=[("bar", None)], 

241 ) 

242 

243 

244def test_legacy_custom_optional_reference() -> None: 

245 """Check that optional HTML-based references are expanded and never reported missing.""" 

246 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

247 run_references_test( 

248 url_map={"ok": "ok.html#ok"}, 

249 source='<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>', 

250 output='<p>foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', 

251 ) 

252 

253 

254def test_custom_optional_reference() -> None: 

255 """Check that optional HTML-based references are expanded and never reported missing.""" 

256 run_references_test( 

257 url_map={"ok": "ok.html#ok"}, 

258 source='<autoref optional identifier="bar">foo</autoref> <autoref identifier=ok optional>ok</autoref>', 

259 output='<p>foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', 

260 ) 

261 

262 

263def test_legacy_custom_optional_hover_reference() -> None: 

264 """Check that optional-hover HTML-based references are expanded and never reported missing.""" 

265 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

266 run_references_test( 

267 url_map={"ok": "ok.html#ok"}, 

268 source='<span data-autorefs-optional-hover="bar">foo</span> <span data-autorefs-optional-hover=ok>ok</span>', 

269 output='<p><span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a></p>', 

270 ) 

271 

272 

273def test_custom_optional_hover_reference() -> None: 

274 """Check that optional-hover HTML-based references are expanded and never reported missing.""" 

275 run_references_test( 

276 url_map={"ok": "ok.html#ok"}, 

277 source='<autoref optional hover identifier="bar">foo</autoref> <autoref optional identifier=ok hover>ok</autoref>', 

278 output='<p><span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a></p>', 

279 ) 

280 

281 

282def test_legacy_external_references() -> None: 

283 """Check that external references are marked as such.""" 

284 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

285 run_references_test( 

286 url_map={"example": "https://example.com/#example"}, 

287 source='<span data-autorefs-optional="example">example</span>', 

288 output='<p><a class="autorefs autorefs-external" href="https://example.com/#example">example</a></p>', 

289 ) 

290 

291 

292def test_external_references() -> None: 

293 """Check that external references are marked as such.""" 

294 run_references_test( 

295 url_map={"example": "https://example.com/#example"}, 

296 source='<autoref optional identifier="example">example</autoref>', 

297 output='<p><a class="autorefs autorefs-external" href="https://example.com/#example">example</a></p>', 

298 ) 

299 

300 

301def test_register_markdown_anchors() -> None: 

302 """Check that Markdown anchors are registered when enabled.""" 

303 plugin = AutorefsPlugin() 

304 md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) 

305 plugin.current_page = "page" 

306 md.convert( 

307 dedent( 

308 """ 

309 [](){#foo} 

310 ## Heading foo 

311 

312 Paragraph 1. 

313 

314 [](){#bar} 

315 Paragraph 2. 

316 

317 [](){#alias1} 

318 [](){#alias2} 

319 ## Heading bar 

320 

321 [](){#alias3} 

322 Text. 

323 [](){#alias4} 

324 ## Heading baz 

325 

326 [](){#alias5} 

327 [](){#alias6} 

328 Decoy. 

329 ## Heading more1 

330 

331 [](){#alias7} 

332 [decoy](){#alias8} 

333 [](){#alias9} 

334 ## Heading more2 {#heading-custom2} 

335 

336 [](){#aliasSame} 

337 ## Same heading 1 

338 [](){#aliasSame} 

339 ## Same heading 2 

340 

341 [](){#alias10} 

342 """, 

343 ), 

344 ) 

345 assert plugin._primary_url_map == { 

346 "foo": ["page#heading-foo"], 

347 "bar": ["page#bar"], 

348 "alias1": ["page#heading-bar"], 

349 "alias2": ["page#heading-bar"], 

350 "alias3": ["page#alias3"], 

351 "alias4": ["page#heading-baz"], 

352 "alias5": ["page#alias5"], 

353 "alias6": ["page#alias6"], 

354 "alias7": ["page#alias7"], 

355 "alias8": ["page#alias8"], 

356 "alias9": ["page#heading-custom2"], 

357 "alias10": ["page#alias10"], 

358 "aliasSame": ["page#same-heading-1", "page#same-heading-2"], 

359 } 

360 

361 

362def test_register_markdown_anchors_with_admonition() -> None: 

363 """Check that Markdown anchors are registered inside a nested admonition element.""" 

364 plugin = AutorefsPlugin() 

365 md = markdown.Markdown(extensions=["attr_list", "toc", "admonition", AutorefsExtension(plugin)]) 

366 plugin.current_page = "page" 

367 md.convert( 

368 dedent( 

369 """ 

370 [](){#alias1} 

371 !!! note 

372 ## Heading foo 

373 

374 [](){#alias2} 

375 ## Heading bar 

376 

377 [](){#alias3} 

378 ## Heading baz 

379 """, 

380 ), 

381 ) 

382 assert plugin._primary_url_map == { 

383 "alias1": ["page#alias1"], 

384 "alias2": ["page#heading-bar"], 

385 "alias3": ["page#alias3"], 

386 } 

387 

388 

389def test_legacy_keep_data_attributes() -> None: 

390 """Keep HTML data attributes from autorefs spans.""" 

391 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

392 run_references_test( 

393 url_map={"example": "https://e.com/#example"}, 

394 source='<span data-autorefs-optional="example" class="hi ho" data-foo data-bar="0">e</span>', 

395 output='<p><a class="autorefs autorefs-external hi ho" href="https://e.com/#example" data-foo data-bar="0">e</a></p>', 

396 ) 

397 

398 

399def test_keep_data_attributes() -> None: 

400 """Keep HTML data attributes from autorefs spans.""" 

401 run_references_test( 

402 url_map={"example": "https://e.com#a"}, 

403 source='<autoref optional identifier="example" class="hi ho" data-foo data-bar="0">e</autoref>', 

404 output='<p><a class="autorefs autorefs-external hi ho" href="https://e.com#a" data-foo data-bar="0">e</a></p>', 

405 ) 

406 

407 

408@pytest.mark.parametrize( 

409 ("markdown_ref", "exact_expected"), 

410 [ 

411 ("[Foo][]", False), 

412 ("[\\`Foo][]", False), 

413 ("[\\`\\`Foo][]", False), 

414 ("[\\`\\`Foo\\`][]", False), 

415 ("[Foo\\`][]", False), 

416 ("[Foo\\`\\`][]", False), 

417 ("[\\`Foo\\`\\`][]", False), 

418 ("[`Foo` `Bar`][]", False), 

419 ("[Foo][Foo]", True), 

420 ("[`Foo`][]", True), 

421 ("[`Foo``Bar`][]", True), 

422 ("[`Foo```Bar`][]", True), 

423 ("[``Foo```Bar``][]", True), 

424 ("[``Foo`Bar``][]", True), 

425 ("[```Foo``Bar```][]", True), 

426 ], 

427) 

428def test_mark_identifiers_as_exact(markdown_ref: str, exact_expected: bool) -> None: 

429 """Mark code and explicit identifiers as exact (no `slug` attribute in autoref elements).""" 

430 plugin = AutorefsPlugin() 

431 md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) 

432 plugin.current_page = "page" 

433 output = md.convert(markdown_ref) 

434 if exact_expected: 

435 assert "slug=" not in output 

436 else: 

437 assert "slug=" in output 

438 

439 

440def test_slugified_identifier_fallback() -> None: 

441 """Fallback to the slugified identifier when no URL is found.""" 

442 run_references_test( 

443 url_map={"hello-world": "https://e.com#a"}, 

444 source='<autoref identifier="Hello World" slug="hello-world">Hello World</autoref>', 

445 output='<p><a class="autorefs autorefs-external" href="https://e.com#a">Hello World</a></p>', 

446 ) 

447 run_references_test( 

448 url_map={"foo-bar": "https://e.com#a"}, 

449 source="[*Foo*-bar][]", 

450 output='<p><a class="autorefs autorefs-external" href="https://e.com#a"><em>Foo</em>-bar</a></p>', 

451 ) 

452 run_references_test( 

453 url_map={"foo-bar": "https://e.com#a"}, 

454 source="[`Foo`-bar][]", 

455 output='<p><a class="autorefs autorefs-external" href="https://e.com#a"><code>Foo</code>-bar</a></p>', 

456 ) 

457 

458 

459def test_no_fallback_for_exact_identifiers() -> None: 

460 """Do not fallback to the slugified identifier for exact identifiers.""" 

461 run_references_test( 

462 url_map={"hello-world": "https://e.com"}, 

463 source='<autoref identifier="Hello World"><code>Hello World</code></autoref>', 

464 output="<p>[<code>Hello World</code>][]</p>", 

465 unmapped=[("Hello World", None)], 

466 ) 

467 

468 run_references_test( 

469 url_map={"hello-world": "https://e.com"}, 

470 source='<autoref identifier="Hello World">Hello World</autoref>', 

471 output="<p>[Hello World][]</p>", 

472 unmapped=[("Hello World", None)], 

473 ) 

474 

475 

476def test_no_fallback_for_provided_identifiers() -> None: 

477 """Do not slugify provided identifiers.""" 

478 run_references_test( 

479 url_map={"hello-world": "foo.html#hello-world"}, 

480 source="[Hello][Hello world]", 

481 output="<p>[Hello][Hello world]</p>", 

482 unmapped=[("Hello world", None)], 

483 )