From 6613e4aa7edd5874862a6fe50db052501b3d8107 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Tue, 20 Apr 2021 22:02:13 +0200 Subject: [PATCH] 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. --- src/mkdocs_autorefs/plugin.py | 49 +++++++++++++++++++++++-------- src/mkdocs_autorefs/references.py | 14 +++------ tests/test_plugin.py | 42 ++++++++++++++++++++++++++ tests/test_references.py | 15 +++++++++- 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 tests/test_plugin.py diff --git a/src/mkdocs_autorefs/plugin.py b/src/mkdocs_autorefs/plugin.py index 3d5ba32..fe674da 100644 --- a/src/mkdocs_autorefs/plugin.py +++ b/src/mkdocs_autorefs/plugin.py @@ -10,6 +10,7 @@ and fixes them using the previously stored identifier-URL mapping. """ +import functools import logging from typing import Callable, Dict, Optional @@ -19,7 +20,7 @@ from mkdocs.structure.toc import AnchorLink from mkdocs.utils import warning_filter -from mkdocs_autorefs.references import AutorefsExtension, fix_refs +from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url log = logging.getLogger(f"mkdocs.plugins.{__name__}") log.addFilter(warning_filter) @@ -45,22 +46,36 @@ def __init__(self) -> None: """Initialize the object.""" super().__init__() self._url_map: Dict[str, str] = {} - self.get_fallback_anchor: Callable[[str], Optional[str]] = lambda identifier: None + self._abs_url_map: Dict[str, str] = {} + self.get_fallback_anchor: Optional[Callable[[str], Optional[str]]] = None - def register_anchor(self, page: str, anchor: str): + def register_anchor(self, page: str, identifier: str): """Register that an anchor corresponding to an identifier was encountered when rendering the page. Arguments: page: The relative URL of the current page. Examples: `'foo/bar/'`, `'foo/index.html'` - anchor: The HTML anchor (without '#') as a string. + identifier: The HTML anchor (without '#') as a string. """ - self._url_map[anchor] = f"{page}#{anchor}" + self._url_map[identifier] = f"{page}#{identifier}" - def get_item_url(self, anchor: str) -> str: + def register_url(self, identifier: str, url: str): + """Register that the identifier should be turned into a link to this URL. + + Arguments: + identifier: The new identifier. + url: The absolute URL (including anchor, if needed) where this item can be found. + """ + self._abs_url_map[identifier] = url + + def get_item_url( + self, identifier: str, from_url: Optional[str] = None, fallback: Optional[Callable[[str], Optional[str]]] = None + ) -> str: """Return a site-relative URL with anchor to the identifier, if it's present anywhere. Arguments: - anchor: The anchor (without '#'). + identifier: The anchor (without '#'). + from_url: The URL of the base page, from which we link towards the targeted pages. + fallback: An optional function to suggest an alternative anchor to try on failure. Returns: A site-relative URL. @@ -69,13 +84,22 @@ def get_item_url(self, anchor: str) -> str: KeyError: If there isn't an item by this identifier anywhere on the site. """ try: - return self._url_map[anchor] + url = self._url_map[identifier] except KeyError: - new_anchor = self.get_fallback_anchor(anchor) - if new_anchor and new_anchor in self._url_map: - return self._url_map[new_anchor] + if identifier in self._abs_url_map: + return self._abs_url_map[identifier] + + if fallback: + new_identifier = fallback(identifier) + if new_identifier: + return self.get_item_url(new_identifier, from_url) + raise + if from_url is not None: + return relative_url(from_url, url) + return url + def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613,R0201 (unused arguments, cannot be static) """Instantiate our Markdown extension. @@ -166,7 +190,8 @@ def on_post_page(self, output: str, page: Page, **kwargs) -> str: # noqa: W0613 """ log.debug(f"{__name__}: Fixing references in page {page.file.src_path}") - fixed_output, unmapped = fix_refs(output, page.url, self.get_item_url) + url_mapper = functools.partial(self.get_item_url, from_url=page.url, fallback=self.get_fallback_anchor) + fixed_output, unmapped = fix_refs(output, url_mapper) if unmapped and log.isEnabledFor(logging.WARNING): for ref in unmapped: diff --git a/src/mkdocs_autorefs/references.py b/src/mkdocs_autorefs/references.py index 2d622c1..2180353 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -125,7 +125,7 @@ def relative_url(url_a: str, url_b: str) -> str: return f"{relative}#{anchor}" -def fix_ref(url_mapper: Callable[[str], str], from_url: str, unmapped: List[str]) -> Callable: +def fix_ref(url_mapper: Callable[[str], str], unmapped: List[str]) -> Callable: """Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub). In our context, we match Markdown references and replace them with HTML links. @@ -137,7 +137,6 @@ def fix_ref(url_mapper: Callable[[str], str], from_url: str, unmapped: List[str] Arguments: url_mapper: A callable that gets an object's site URL by its identifier, such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][]. - from_url: The URL of the base page, from which we link towards the targeted pages. unmapped: A list to store unmapped identifiers. Returns: @@ -150,7 +149,7 @@ def inner(match: Match): title = match["title"] try: - url = relative_url(from_url, url_mapper(unescape(identifier))) + url = url_mapper(unescape(identifier)) except KeyError: unmapped.append(identifier) if title == identifier: @@ -162,16 +161,11 @@ def inner(match: Match): return inner -def fix_refs( - html: str, - from_url: str, - url_mapper: Callable[[str], str], -) -> Tuple[str, List[str]]: +def fix_refs(html: str, url_mapper: Callable[[str], str]) -> Tuple[str, List[str]]: """Fix all references in the given HTML text. Arguments: html: The text to fix. - from_url: The URL at which this HTML is served. url_mapper: A callable that gets an object's site URL by its identifier, such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][]. @@ -179,7 +173,7 @@ def fix_refs( The fixed HTML. """ unmapped = [] # type: ignore - html = AUTO_REF_RE.sub(fix_ref(url_mapper, from_url, unmapped), html) + html = AUTO_REF_RE.sub(fix_ref(url_mapper, unmapped), html) return html, unmapped diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..5c320d5 --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,42 @@ +"""Tests for the plugin module.""" +import pytest + +from mkdocs_autorefs.plugin import AutorefsPlugin + + +def test_url_registration(): + """Check that URLs can be registered, then obtained.""" + plugin = AutorefsPlugin() + plugin.register_anchor(identifier="foo", page="foo1.html") + plugin.register_url(identifier="bar", url="https://example.org/bar.html") + + assert plugin.get_item_url("foo") == "foo1.html#foo" + assert plugin.get_item_url("bar") == "https://example.org/bar.html" + with pytest.raises(KeyError): + plugin.get_item_url("baz") + + +def test_url_registration_with_from_url(): + """Check that URLs can be registered, then obtained, relative to a page.""" + plugin = AutorefsPlugin() + plugin.register_anchor(identifier="foo", page="foo1.html") + plugin.register_url(identifier="bar", url="https://example.org/bar.html") + + assert plugin.get_item_url("foo", from_url="a/b.html") == "../foo1.html#foo" + assert plugin.get_item_url("bar", from_url="a/b.html") == "https://example.org/bar.html" + with pytest.raises(KeyError): + plugin.get_item_url("baz", from_url="a/b.html") + + +def test_url_registration_with_fallback(): + """Check that URLs can be registered, then obtained through a fallback.""" + plugin = AutorefsPlugin() + plugin.register_anchor(identifier="foo", page="foo1.html") + plugin.register_url(identifier="bar", url="https://example.org/bar.html") + + assert plugin.get_item_url("baz", fallback=lambda s: "foo") == "foo1.html#foo" + assert plugin.get_item_url("baz", fallback=lambda s: "bar") == "https://example.org/bar.html" + with pytest.raises(KeyError): + plugin.get_item_url("baz", fallback=lambda s: "baaaa") + with pytest.raises(KeyError): + plugin.get_item_url("baz", fallback=lambda s: None) diff --git a/tests/test_references.py b/tests/test_references.py index 7a2c020..d819d08 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -48,7 +48,10 @@ def run_references_test(url_map, source, output, unmapped=None, from_url="page.h md = markdown.Markdown(extensions=[AutorefsExtension()]) content = md.convert(source) - actual_output, actual_unmapped = fix_refs(content, from_url, url_map.__getitem__) + def url_mapper(identifier): + return relative_url(from_url, url_map[identifier]) + + actual_output, actual_unmapped = fix_refs(content, url_mapper) assert actual_output == output assert actual_unmapped == (unmapped or []) @@ -89,6 +92,16 @@ def test_reference_with_punctuation(): ) +def test_reference_to_relative_path(): + """Check references from a page at a nested path.""" + run_references_test( + from_url="sub/sub/page.html", + url_map={"zz": "foo.html#zz"}, + source="This [zz][].", + output='

This zz.

', + ) + + def test_no_reference_with_space(): """Check that references with spaces are not fixed.""" run_references_test(