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
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-24 13:40 +0100
1"""Tests for the references module."""
3from __future__ import annotations
5from textwrap import dedent
6from typing import TYPE_CHECKING, Any
8import markdown
9import pytest
11from mkdocs_autorefs import AutorefsExtension, AutorefsHookInterface, AutorefsPlugin, fix_refs, relative_url
12from tests.helpers import create_page
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: 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.
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 {}
73 def url_mapper(identifier: str) -> tuple[str, str | None]:
74 return relative_url(from_url, url_map[identifier]), title_map.get(identifier, None)
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 [])
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 )
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 )
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 )
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 )
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 )
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&"bar">Foo&"bar</a>.</p>',
134 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 = create_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 = create_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 }
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 )
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 )
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
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 )
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 )
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 )
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 )
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 )
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 )