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
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-10 16:32 +0100
1"""Tests for the references module."""
3from __future__ import annotations
5from textwrap import dedent
6from typing import TYPE_CHECKING
8import markdown
9import pytest
11from mkdocs_autorefs.plugin import AutorefsPlugin
12from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url
14if TYPE_CHECKING:
15 from collections.abc import Mapping
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
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.
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)
68 def url_mapper(identifier: str) -> str:
69 return relative_url(from_url, url_map[identifier])
71 actual_output, actual_unmapped = fix_refs(content, url_mapper)
72 assert actual_output == output
73 assert actual_unmapped == (unmapped or [])
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 )
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 )
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 )
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 )
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 )
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&"bar">Foo&"bar</a>.</p>',
129 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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
312 Paragraph 1.
314 [](){#bar}
315 Paragraph 2.
317 [](){#alias1}
318 [](){#alias2}
319 ## Heading bar
321 [](){#alias3}
322 Text.
323 [](){#alias4}
324 ## Heading baz
326 [](){#alias5}
327 [](){#alias6}
328 Decoy.
329 ## Heading more1
331 [](){#alias7}
332 [decoy](){#alias8}
333 [](){#alias9}
334 ## Heading more2 {#heading-custom2}
336 [](){#aliasSame}
337 ## Same heading 1
338 [](){#aliasSame}
339 ## Same heading 2
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 }
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
374 [](){#alias2}
375 ## Heading bar
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 }
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 )
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 )
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
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 )
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 )
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 )
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 )