Coverage for tests/test_references.py: 100.00%
124 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-01 20:28 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-01 20:28 +0200
1"""Tests for the references module."""
3from __future__ import annotations
5from textwrap import dedent
6from typing import Mapping
8import markdown
9import pytest
11from mkdocs_autorefs.plugin import AutorefsPlugin
12from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url
15@pytest.mark.parametrize(
16 ("current_url", "to_url", "href_url"),
17 [
18 ("a/", "a#b", "#b"),
19 ("a/", "a/b#c", "b#c"),
20 ("a/b/", "a/b#c", "#c"),
21 ("a/b/", "a/c#d", "../c#d"),
22 ("a/b/", "a#c", "..#c"),
23 ("a/b/c/", "d#e", "../../../d#e"),
24 ("a/b/", "c/d/#e", "../../c/d/#e"),
25 ("a/index.html", "a/index.html#b", "#b"),
26 ("a/index.html", "a/b.html#c", "b.html#c"),
27 ("a/b.html", "a/b.html#c", "#c"),
28 ("a/b.html", "a/c.html#d", "c.html#d"),
29 ("a/b.html", "a/index.html#c", "index.html#c"),
30 ("a/b/c.html", "d.html#e", "../../d.html#e"),
31 ("a/b.html", "c/d.html#e", "../c/d.html#e"),
32 ("a/b/index.html", "a/b/c/d.html#e", "c/d.html#e"),
33 ("", "#x", "#x"),
34 ("a/", "#x", "../#x"),
35 ("a/b.html", "#x", "../#x"),
36 ("", "a/#x", "a/#x"),
37 ("", "a/b.html#x", "a/b.html#x"),
38 ],
39)
40def test_relative_url(current_url: str, to_url: str, href_url: str) -> None:
41 """Compute relative URLs correctly."""
42 assert relative_url(current_url, to_url) == href_url
45def run_references_test(
46 url_map: dict[str, str],
47 source: str,
48 output: str,
49 unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None,
50 from_url: str = "page.html",
51 extensions: Mapping = {},
52) -> None:
53 """Help running tests about references.
55 Arguments:
56 url_map: The URL mapping.
57 source: The source text.
58 output: The expected output.
59 unmapped: The expected unmapped list.
60 from_url: The source page URL.
61 """
62 md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions)
63 content = md.convert(source)
65 def url_mapper(identifier: str) -> str:
66 return relative_url(from_url, url_map[identifier])
68 actual_output, actual_unmapped = fix_refs(content, url_mapper)
69 assert actual_output == output
70 assert actual_unmapped == (unmapped or [])
73def test_reference_implicit() -> None:
74 """Check implicit references (identifier only)."""
75 run_references_test(
76 url_map={"Foo": "foo.html#Foo"},
77 source="This [Foo][].",
78 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo">Foo</a>.</p>',
79 )
82def test_reference_explicit_with_markdown_text() -> None:
83 """Check explicit references with Markdown formatting."""
84 run_references_test(
85 url_map={"Foo": "foo.html#Foo"},
86 source="This [**Foo**][Foo].",
87 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><strong>Foo</strong></a>.</p>',
88 )
91def test_reference_implicit_with_code() -> None:
92 """Check implicit references (identifier only, wrapped in backticks)."""
93 run_references_test(
94 url_map={"Foo": "foo.html#Foo"},
95 source="This [`Foo`][].",
96 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>Foo</code></a>.</p>',
97 )
100def test_reference_implicit_with_code_inlinehilite_plain() -> None:
101 """Check implicit references (identifier in backticks, wrapped by inlinehilite)."""
102 run_references_test(
103 extensions={"pymdownx.inlinehilite": {}},
104 url_map={"pathlib.Path": "pathlib.html#Path"},
105 source="This [`pathlib.Path`][].",
106 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code>pathlib.Path</code></a>.</p>',
107 )
110def test_reference_implicit_with_code_inlinehilite_python() -> None:
111 """Check implicit references (identifier in backticks, syntax-highlighted by inlinehilite)."""
112 run_references_test(
113 extensions={"pymdownx.inlinehilite": {"style_plain_text": "python"}, "pymdownx.highlight": {}},
114 url_map={"pathlib.Path": "pathlib.html#Path"},
115 source="This [`pathlib.Path`][].",
116 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code class="highlight">pathlib.Path</code></a>.</p>',
117 )
120def test_reference_with_punctuation() -> None:
121 """Check references with punctuation."""
122 run_references_test(
123 url_map={'Foo&"bar': 'foo.html#Foo&"bar'},
124 source='This [Foo&"bar][].',
125 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo&"bar">Foo&"bar</a>.</p>',
126 )
129def test_reference_to_relative_path() -> None:
130 """Check references from a page at a nested path."""
131 run_references_test(
132 from_url="sub/sub/page.html",
133 url_map={"zz": "foo.html#zz"},
134 source="This [zz][].",
135 output='<p>This <a class="autorefs autorefs-internal" href="../../foo.html#zz">zz</a>.</p>',
136 )
139def test_multiline_links() -> None:
140 """Check that links with multiline text are recognized."""
141 run_references_test(
142 url_map={"foo-bar": "foo.html#bar"},
143 source="This [Foo\nbar][foo-bar].",
144 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo\nbar</a>.</p>',
145 )
148def test_no_reference_with_space() -> None:
149 """Check that references with spaces are fixed."""
150 run_references_test(
151 url_map={"Foo bar": "foo.html#bar"},
152 source="This [Foo bar][].",
153 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo bar</a>.</p>',
154 )
157def test_no_reference_inside_markdown() -> None:
158 """Check that references inside code are not fixed."""
159 run_references_test(
160 url_map={"Foo": "foo.html#Foo"},
161 source="This `[Foo][]`.",
162 output="<p>This <code>[Foo][]</code>.</p>",
163 )
166def test_missing_reference() -> None:
167 """Check that implicit references are correctly seen as unmapped."""
168 run_references_test(
169 url_map={"NotFoo": "foo.html#NotFoo"},
170 source="[Foo][]",
171 output="<p>[Foo][]</p>",
172 unmapped=[("Foo", None)],
173 )
176def test_missing_reference_with_markdown_text() -> None:
177 """Check unmapped explicit references."""
178 run_references_test(
179 url_map={"NotFoo": "foo.html#NotFoo"},
180 source="[`Foo`][Foo]",
181 output="<p>[<code>Foo</code>][Foo]</p>",
182 unmapped=[("Foo", None)],
183 )
186def test_missing_reference_with_markdown_id() -> None:
187 """Check unmapped explicit references with Markdown in the identifier."""
188 run_references_test(
189 url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"},
190 source="[Foo][*NotFoo*]",
191 output="<p>[Foo][*NotFoo*]</p>",
192 unmapped=[("*NotFoo*", None)],
193 )
196def test_missing_reference_with_markdown_implicit() -> None:
197 """Check that implicit references are not fixed when the identifier is not the exact one."""
198 run_references_test(
199 url_map={"Foo-bar": "foo.html#Foo-bar"},
200 source="[*Foo-bar*][] and [`Foo`-bar][]",
201 output="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][]</p>",
202 unmapped=[("*Foo-bar*", None)],
203 )
206def test_reference_with_markup() -> None:
207 """Check that references with markup are resolved (and need escaping to prevent rendering)."""
208 run_references_test(
209 url_map={"*a b*": "foo.html#Foo"},
210 source="This [*a b*][].",
211 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><em>a b</em></a>.</p>',
212 )
213 run_references_test(
214 url_map={"*a/b*": "foo.html#Foo"},
215 source="This [`*a/b*`][].",
216 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>*a/b*</code></a>.</p>',
217 )
220def test_legacy_custom_required_reference() -> None:
221 """Check that external HTML-based references are expanded or reported missing."""
222 url_map = {"ok": "ok.html#ok"}
223 source = "<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>"
224 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
225 output, unmapped = fix_refs(source, url_map.__getitem__)
226 assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
227 assert unmapped == [("bar", None)]
230def test_custom_required_reference() -> None:
231 """Check that external HTML-based references are expanded or reported missing."""
232 url_map = {"ok": "ok.html#ok"}
233 source = "<autoref identifier=bar>foo</autoref> <autoref identifier=ok>ok</autoref>"
234 output, unmapped = fix_refs(source, url_map.__getitem__)
235 assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
236 assert unmapped == [("bar", None)]
239def test_legacy_custom_optional_reference() -> None:
240 """Check that optional HTML-based references are expanded and never reported missing."""
241 url_map = {"ok": "ok.html#ok"}
242 source = '<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>'
243 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
244 output, unmapped = fix_refs(source, url_map.__getitem__)
245 assert output == 'foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
246 assert unmapped == []
249def test_custom_optional_reference() -> None:
250 """Check that optional HTML-based references are expanded and never reported missing."""
251 url_map = {"ok": "ok.html#ok"}
252 source = '<autoref optional identifier="bar">foo</autoref> <autoref identifier=ok optional>ok</autoref>'
253 output, unmapped = fix_refs(source, url_map.__getitem__)
254 assert output == 'foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
255 assert unmapped == []
258def test_legacy_custom_optional_hover_reference() -> None:
259 """Check that optional-hover HTML-based references are expanded and never reported missing."""
260 url_map = {"ok": "ok.html#ok"}
261 source = '<span data-autorefs-optional-hover="bar">foo</span> <span data-autorefs-optional-hover=ok>ok</span>'
262 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
263 output, unmapped = fix_refs(source, url_map.__getitem__)
264 assert (
265 output
266 == '<span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a>'
267 )
268 assert unmapped == []
271def test_custom_optional_hover_reference() -> None:
272 """Check that optional-hover HTML-based references are expanded and never reported missing."""
273 url_map = {"ok": "ok.html#ok"}
274 source = '<autoref optional hover identifier="bar">foo</autoref> <autoref optional identifier=ok hover>ok</autoref>'
275 output, unmapped = fix_refs(source, url_map.__getitem__)
276 assert (
277 output
278 == '<span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a>'
279 )
280 assert unmapped == []
283def test_legacy_external_references() -> None:
284 """Check that external references are marked as such."""
285 url_map = {"example": "https://example.com"}
286 source = '<span data-autorefs-optional="example">example</span>'
287 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
288 output, unmapped = fix_refs(source, url_map.__getitem__)
289 assert output == '<a class="autorefs autorefs-external" href="https://example.com">example</a>'
290 assert unmapped == []
293def test_external_references() -> None:
294 """Check that external references are marked as such."""
295 url_map = {"example": "https://example.com"}
296 source = '<autoref optional identifier="example">example</autoref>'
297 output, unmapped = fix_refs(source, url_map.__getitem__)
298 assert output == '<a class="autorefs autorefs-external" href="https://example.com">example</a>'
299 assert unmapped == []
302def test_register_markdown_anchors() -> None:
303 """Check that Markdown anchors are registered when enabled."""
304 plugin = AutorefsPlugin()
305 md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)])
306 plugin.current_page = "page"
307 md.convert(
308 dedent(
309 """
310 [](){#foo}
311 ## Heading foo
313 Paragraph 1.
315 [](){#bar}
316 Paragraph 2.
318 [](){#alias1}
319 [](){#alias2}
320 ## Heading bar
322 [](){#alias3}
323 Text.
324 [](){#alias4}
325 ## Heading baz
327 [](){#alias5}
328 [](){#alias6}
329 Decoy.
330 ## Heading more1
332 [](){#alias7}
333 [decoy](){#alias8}
334 [](){#alias9}
335 ## Heading more2 {#heading-custom2}
337 [](){#aliasSame}
338 ## Same heading 1
339 [](){#aliasSame}
340 ## Same heading 2
342 [](){#alias10}
343 """,
344 ),
345 )
346 assert plugin._url_map == {
347 "foo": ["page#heading-foo"],
348 "bar": ["page#bar"],
349 "alias1": ["page#heading-bar"],
350 "alias2": ["page#heading-bar"],
351 "alias3": ["page#alias3"],
352 "alias4": ["page#heading-baz"],
353 "alias5": ["page#alias5"],
354 "alias6": ["page#alias6"],
355 "alias7": ["page#alias7"],
356 "alias8": ["page#alias8"],
357 "alias9": ["page#heading-custom2"],
358 "alias10": ["page#alias10"],
359 "aliasSame": ["page#same-heading-1", "page#same-heading-2"],
360 }
363def test_register_markdown_anchors_with_admonition() -> None:
364 """Check that Markdown anchors are registered inside a nested admonition element."""
365 plugin = AutorefsPlugin()
366 md = markdown.Markdown(extensions=["attr_list", "toc", "admonition", AutorefsExtension(plugin)])
367 plugin.current_page = "page"
368 md.convert(
369 dedent(
370 """
371 [](){#alias1}
372 !!! note
373 ## Heading foo
375 [](){#alias2}
376 ## Heading bar
378 [](){#alias3}
379 ## Heading baz
380 """,
381 ),
382 )
383 assert plugin._url_map == {
384 "alias1": ["page#alias1"],
385 "alias2": ["page#heading-bar"],
386 "alias3": ["page#alias3"],
387 }
390def test_legacy_keep_data_attributes() -> None:
391 """Keep HTML data attributes from autorefs spans."""
392 url_map = {"example": "https://e.com"}
393 source = '<span data-autorefs-optional="example" class="hi ho" data-foo data-bar="0">e</span>'
394 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
395 output, _ = fix_refs(source, url_map.__getitem__)
396 assert output == '<a class="autorefs autorefs-external hi ho" href="https://e.com" data-foo data-bar="0">e</a>'
399def test_keep_data_attributes() -> None:
400 """Keep HTML data attributes from autorefs spans."""
401 url_map = {"example": "https://e.com"}
402 source = '<autoref optional identifier="example" class="hi ho" data-foo data-bar="0">e</autoref>'
403 output, _ = fix_refs(source, url_map.__getitem__)
404 assert output == '<a class="autorefs autorefs-external hi ho" href="https://e.com" data-foo data-bar="0">e</a>'