Coverage for tests/test_references.py: 100.00%

108 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-24 13:40 +0100

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

2 

3from __future__ import annotations 

4 

5from textwrap import dedent 

6from typing import TYPE_CHECKING, Any 

7 

8import markdown 

9import pytest 

10 

11from mkdocs_autorefs import AutorefsExtension, AutorefsHookInterface, AutorefsPlugin, fix_refs, relative_url 

12from tests.helpers import create_page 

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: Mapping[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[str, Mapping[str, Any]] | None = None, 

55 title_map: Mapping[str, str] | None = None, 

56 *, 

57 strip_tags: bool = True, 

58) -> None: 

59 """Help running tests about references. 

60 

61 Arguments: 

62 url_map: The URL mapping. 

63 source: The source text. 

64 output: The expected output. 

65 unmapped: The expected unmapped list. 

66 from_url: The source page URL. 

67 """ 

68 extensions = extensions or {} 

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

70 content = md.convert(source) 

71 title_map = title_map or {} 

72 

73 def url_mapper(identifier: str) -> tuple[str, str | None]: 

74 return relative_url(from_url, url_map[identifier]), title_map.get(identifier, None) 

75 

76 actual_output, actual_unmapped = fix_refs(content, url_mapper, strip_title_tags=strip_tags) 

77 assert actual_output == output 

78 assert actual_unmapped == (unmapped or []) 

79 

80 

81def test_reference_implicit() -> None: 

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

83 run_references_test( 

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

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

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

87 ) 

88 

89 

90def test_reference_explicit_with_markdown_text() -> None: 

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

92 run_references_test( 

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

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

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

96 ) 

97 

98 

99def test_reference_implicit_with_code() -> None: 

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

101 run_references_test( 

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

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

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

105 ) 

106 

107 

108def test_reference_implicit_with_code_inlinehilite_plain() -> None: 

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

110 run_references_test( 

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

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

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

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

115 ) 

116 

117 

118def test_reference_implicit_with_code_inlinehilite_python() -> None: 

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

120 run_references_test( 

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

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

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

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

125 ) 

126 

127 

128def test_reference_with_punctuation() -> None: 

129 """Check references with punctuation.""" 

130 run_references_test( 

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

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

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

134 ) 

135 

136 

137def test_reference_to_relative_path() -> None: 

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

139 run_references_test( 

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

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

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

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

144 ) 

145 

146 

147def test_multiline_links() -> None: 

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

149 run_references_test( 

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

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

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

153 ) 

154 

155 

156def test_no_reference_with_space() -> None: 

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

158 run_references_test( 

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

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

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

162 ) 

163 

164 

165def test_no_reference_inside_markdown() -> None: 

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

167 run_references_test( 

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

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

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

171 ) 

172 

173 

174def test_missing_reference() -> None: 

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

176 run_references_test( 

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

178 source="[Foo][]", 

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

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

181 ) 

182 

183 

184def test_missing_reference_with_markdown_text() -> None: 

185 """Check unmapped explicit references.""" 

186 run_references_test( 

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

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

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

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

191 ) 

192 

193 

194def test_missing_reference_with_markdown_id() -> None: 

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

196 run_references_test( 

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

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

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

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

201 ) 

202 

203 

204def test_missing_reference_with_markdown_implicit() -> None: 

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

206 run_references_test( 

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

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

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

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

211 ) 

212 

213 

214def test_reference_with_markup() -> None: 

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

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"><em>a b</em></a>.</p>', 

220 ) 

221 run_references_test( 

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

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

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

225 ) 

226 

227 

228# YORE: Bump 2: Remove block. 

229def test_legacy_custom_required_reference() -> None: 

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

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

232 run_references_test( 

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

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

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

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

237 ) 

238 

239 

240def test_custom_required_reference() -> None: 

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

242 run_references_test( 

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

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

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

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

247 ) 

248 

249 

250# YORE: Bump 2: Remove block. 

251def test_legacy_custom_optional_reference() -> None: 

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

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

254 run_references_test( 

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

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

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

258 ) 

259 

260 

261def test_custom_optional_reference() -> None: 

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

263 run_references_test( 

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

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

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

267 ) 

268 

269 

270# YORE: Bump 2: Remove block. 

271def test_legacy_custom_optional_hover_reference() -> None: 

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

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

274 run_references_test( 

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

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

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

278 ) 

279 

280 

281# YORE: Bump 2: Remove block. 

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 = create_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 = create_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 

389# YORE: Bump 2: Remove block. 

390def test_legacy_keep_data_attributes() -> None: 

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

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

393 run_references_test( 

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

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

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

397 ) 

398 

399 

400def test_keep_data_attributes() -> None: 

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

402 run_references_test( 

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

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

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

406 ) 

407 

408 

409@pytest.mark.parametrize( 

410 ("markdown_ref", "exact_expected"), 

411 [ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

427 ], 

428) 

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

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

431 plugin = AutorefsPlugin() 

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

433 plugin.current_page = create_page("page") 

434 output = md.convert(markdown_ref) 

435 if exact_expected: 

436 assert "slug=" not in output 

437 else: 

438 assert "slug=" in output 

439 

440 

441def test_slugified_identifier_fallback() -> None: 

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

443 run_references_test( 

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

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

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

447 ) 

448 run_references_test( 

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

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

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

452 ) 

453 run_references_test( 

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

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

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

457 ) 

458 

459 

460def test_no_fallback_for_exact_identifiers() -> None: 

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

462 run_references_test( 

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

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

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

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

467 ) 

468 

469 run_references_test( 

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

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

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

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

474 ) 

475 

476 

477def test_no_fallback_for_provided_identifiers() -> None: 

478 """Do not slugify provided identifiers.""" 

479 run_references_test( 

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

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

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

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

484 ) 

485 

486 

487def test_title_use_identifier() -> None: 

488 """Check that the identifier is used for the title.""" 

489 run_references_test( 

490 url_map={"fully.qualified.name": "ok.html#fully.qualified.name"}, 

491 source='<autoref optional identifier="fully.qualified.name">name</autoref>', 

492 output='<p><a class="autorefs autorefs-internal" title="fully.qualified.name" href="ok.html#fully.qualified.name">name</a></p>', 

493 ) 

494 

495 

496def test_title_append_identifier() -> None: 

497 """Check that the identifier is appended to the title.""" 

498 run_references_test( 

499 url_map={"fully.qualified.name": "ok.html#fully.qualified.name"}, 

500 title_map={"fully.qualified.name": "Qualified Name"}, 

501 source='<autoref optional identifier="fully.qualified.name">name</autoref>', 

502 output='<p><a class="autorefs autorefs-internal" title="Qualified Name (fully.qualified.name)" href="ok.html#fully.qualified.name">name</a></p>', 

503 )