Skip to content

Commit 621686b

Browse files
authored
feat: Allow registering absolute URLs for autorefs
For now this is not used for anything, but the refactor is good for whatever plan we decide to go through regarding inventories. PR #8: #8
1 parent 7619c28 commit 621686b

File tree

4 files changed

+99
-25
lines changed

4 files changed

+99
-25
lines changed

src/mkdocs_autorefs/plugin.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
and fixes them using the previously stored identifier-URL mapping.
1111
"""
1212

13+
import functools
1314
import logging
1415
from typing import Callable, Dict, Optional
1516

@@ -19,7 +20,7 @@
1920
from mkdocs.structure.toc import AnchorLink
2021
from mkdocs.utils import warning_filter
2122

22-
from mkdocs_autorefs.references import AutorefsExtension, fix_refs
23+
from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url
2324

2425
log = logging.getLogger(f"mkdocs.plugins.{__name__}")
2526
log.addFilter(warning_filter)
@@ -45,22 +46,36 @@ def __init__(self) -> None:
4546
"""Initialize the object."""
4647
super().__init__()
4748
self._url_map: Dict[str, str] = {}
48-
self.get_fallback_anchor: Callable[[str], Optional[str]] = lambda identifier: None
49+
self._abs_url_map: Dict[str, str] = {}
50+
self.get_fallback_anchor: Optional[Callable[[str], Optional[str]]] = None
4951

50-
def register_anchor(self, page: str, anchor: str):
52+
def register_anchor(self, page: str, identifier: str):
5153
"""Register that an anchor corresponding to an identifier was encountered when rendering the page.
5254
5355
Arguments:
5456
page: The relative URL of the current page. Examples: `'foo/bar/'`, `'foo/index.html'`
55-
anchor: The HTML anchor (without '#') as a string.
57+
identifier: The HTML anchor (without '#') as a string.
5658
"""
57-
self._url_map[anchor] = f"{page}#{anchor}"
59+
self._url_map[identifier] = f"{page}#{identifier}"
5860

59-
def get_item_url(self, anchor: str) -> str:
61+
def register_url(self, identifier: str, url: str):
62+
"""Register that the identifier should be turned into a link to this URL.
63+
64+
Arguments:
65+
identifier: The new identifier.
66+
url: The absolute URL (including anchor, if needed) where this item can be found.
67+
"""
68+
self._abs_url_map[identifier] = url
69+
70+
def get_item_url(
71+
self, identifier: str, from_url: Optional[str] = None, fallback: Optional[Callable[[str], Optional[str]]] = None
72+
) -> str:
6073
"""Return a site-relative URL with anchor to the identifier, if it's present anywhere.
6174
6275
Arguments:
63-
anchor: The anchor (without '#').
76+
identifier: The anchor (without '#').
77+
from_url: The URL of the base page, from which we link towards the targeted pages.
78+
fallback: An optional function to suggest an alternative anchor to try on failure.
6479
6580
Returns:
6681
A site-relative URL.
@@ -69,13 +84,22 @@ def get_item_url(self, anchor: str) -> str:
6984
KeyError: If there isn't an item by this identifier anywhere on the site.
7085
"""
7186
try:
72-
return self._url_map[anchor]
87+
url = self._url_map[identifier]
7388
except KeyError:
74-
new_anchor = self.get_fallback_anchor(anchor)
75-
if new_anchor and new_anchor in self._url_map:
76-
return self._url_map[new_anchor]
89+
if identifier in self._abs_url_map:
90+
return self._abs_url_map[identifier]
91+
92+
if fallback:
93+
new_identifier = fallback(identifier)
94+
if new_identifier:
95+
return self.get_item_url(new_identifier, from_url)
96+
7797
raise
7898

99+
if from_url is not None:
100+
return relative_url(from_url, url)
101+
return url
102+
79103
def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613,R0201 (unused arguments, cannot be static)
80104
"""Instantiate our Markdown extension.
81105
@@ -166,7 +190,8 @@ def on_post_page(self, output: str, page: Page, **kwargs) -> str: # noqa: W0613
166190
"""
167191
log.debug(f"{__name__}: Fixing references in page {page.file.src_path}")
168192

169-
fixed_output, unmapped = fix_refs(output, page.url, self.get_item_url)
193+
url_mapper = functools.partial(self.get_item_url, from_url=page.url, fallback=self.get_fallback_anchor)
194+
fixed_output, unmapped = fix_refs(output, url_mapper)
170195

171196
if unmapped and log.isEnabledFor(logging.WARNING):
172197
for ref in unmapped:

src/mkdocs_autorefs/references.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def relative_url(url_a: str, url_b: str) -> str:
128128
return f"{relative}#{anchor}"
129129

130130

131-
def fix_ref(url_mapper: Callable[[str], str], from_url: str, unmapped: List[str]) -> Callable:
131+
def fix_ref(url_mapper: Callable[[str], str], unmapped: List[str]) -> Callable:
132132
"""Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).
133133
134134
In our context, we match Markdown references and replace them with HTML links.
@@ -140,7 +140,6 @@ def fix_ref(url_mapper: Callable[[str], str], from_url: str, unmapped: List[str]
140140
Arguments:
141141
url_mapper: A callable that gets an object's site URL by its identifier,
142142
such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
143-
from_url: The URL of the base page, from which we link towards the targeted pages.
144143
unmapped: A list to store unmapped identifiers.
145144
146145
Returns:
@@ -153,7 +152,7 @@ def inner(match: Match):
153152
title = match["title"]
154153

155154
try:
156-
url = relative_url(from_url, url_mapper(unescape(identifier)))
155+
url = url_mapper(unescape(identifier))
157156
except KeyError:
158157
if match["kind"] == "autorefs-optional":
159158
return title
@@ -167,24 +166,19 @@ def inner(match: Match):
167166
return inner
168167

169168

170-
def fix_refs(
171-
html: str,
172-
from_url: str,
173-
url_mapper: Callable[[str], str],
174-
) -> Tuple[str, List[str]]:
169+
def fix_refs(html: str, url_mapper: Callable[[str], str]) -> Tuple[str, List[str]]:
175170
"""Fix all references in the given HTML text.
176171
177172
Arguments:
178173
html: The text to fix.
179-
from_url: The URL at which this HTML is served.
180174
url_mapper: A callable that gets an object's site URL by its identifier,
181175
such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
182176
183177
Returns:
184178
The fixed HTML.
185179
"""
186180
unmapped = [] # type: ignore
187-
html = AUTO_REF_RE.sub(fix_ref(url_mapper, from_url, unmapped), html)
181+
html = AUTO_REF_RE.sub(fix_ref(url_mapper, unmapped), html)
188182
return html, unmapped
189183

190184

tests/test_plugin.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Tests for the plugin module."""
2+
import pytest
3+
4+
from mkdocs_autorefs.plugin import AutorefsPlugin
5+
6+
7+
def test_url_registration():
8+
"""Check that URLs can be registered, then obtained."""
9+
plugin = AutorefsPlugin()
10+
plugin.register_anchor(identifier="foo", page="foo1.html")
11+
plugin.register_url(identifier="bar", url="https://example.org/bar.html")
12+
13+
assert plugin.get_item_url("foo") == "foo1.html#foo"
14+
assert plugin.get_item_url("bar") == "https://example.org/bar.html"
15+
with pytest.raises(KeyError):
16+
plugin.get_item_url("baz")
17+
18+
19+
def test_url_registration_with_from_url():
20+
"""Check that URLs can be registered, then obtained, relative to a page."""
21+
plugin = AutorefsPlugin()
22+
plugin.register_anchor(identifier="foo", page="foo1.html")
23+
plugin.register_url(identifier="bar", url="https://example.org/bar.html")
24+
25+
assert plugin.get_item_url("foo", from_url="a/b.html") == "../foo1.html#foo"
26+
assert plugin.get_item_url("bar", from_url="a/b.html") == "https://example.org/bar.html"
27+
with pytest.raises(KeyError):
28+
plugin.get_item_url("baz", from_url="a/b.html")
29+
30+
31+
def test_url_registration_with_fallback():
32+
"""Check that URLs can be registered, then obtained through a fallback."""
33+
plugin = AutorefsPlugin()
34+
plugin.register_anchor(identifier="foo", page="foo1.html")
35+
plugin.register_url(identifier="bar", url="https://example.org/bar.html")
36+
37+
assert plugin.get_item_url("baz", fallback=lambda s: "foo") == "foo1.html#foo"
38+
assert plugin.get_item_url("baz", fallback=lambda s: "bar") == "https://example.org/bar.html"
39+
with pytest.raises(KeyError):
40+
plugin.get_item_url("baz", fallback=lambda s: "baaaa")
41+
with pytest.raises(KeyError):
42+
plugin.get_item_url("baz", fallback=lambda s: None)

tests/test_references.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ def run_references_test(url_map, source, output, unmapped=None, from_url="page.h
4848
md = markdown.Markdown(extensions=[AutorefsExtension()])
4949
content = md.convert(source)
5050

51-
actual_output, actual_unmapped = fix_refs(content, from_url, url_map.__getitem__)
51+
def url_mapper(identifier):
52+
return relative_url(from_url, url_map[identifier])
53+
54+
actual_output, actual_unmapped = fix_refs(content, url_mapper)
5255
assert actual_output == output
5356
assert actual_unmapped == (unmapped or [])
5457

@@ -89,6 +92,16 @@ def test_reference_with_punctuation():
8992
)
9093

9194

95+
def test_reference_to_relative_path():
96+
"""Check references from a page at a nested path."""
97+
run_references_test(
98+
from_url="sub/sub/page.html",
99+
url_map={"zz": "foo.html#zz"},
100+
source="This [zz][].",
101+
output='<p>This <a href="../../foo.html#zz">zz</a>.</p>',
102+
)
103+
104+
92105
def test_no_reference_with_space():
93106
"""Check that references with spaces are not fixed."""
94107
run_references_test(
@@ -160,7 +173,7 @@ def test_custom_required_reference():
160173
"""Check that external HTML-based references are expanded or reported missing."""
161174
url_map = {"ok": "ok.html#ok"}
162175
source = "<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>"
163-
output, unmapped = fix_refs(source, "page.html", url_map.__getitem__)
176+
output, unmapped = fix_refs(source, url_map.__getitem__)
164177
assert output == '[foo][bar] <a href="ok.html#ok">ok</a>'
165178
assert unmapped == ["bar"]
166179

@@ -169,6 +182,6 @@ def test_custom_optional_reference():
169182
"""Check that optional HTML-based references are expanded and never reported missing."""
170183
url_map = {"ok": "ok.html#ok"}
171184
source = '<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>'
172-
output, unmapped = fix_refs(source, "page.html", url_map.__getitem__)
185+
output, unmapped = fix_refs(source, url_map.__getitem__)
173186
assert output == 'foo <a href="ok.html#ok">ok</a>'
174187
assert unmapped == []

0 commit comments

Comments
 (0)