diff --git a/src/mkdocs_autorefs/plugin.py b/src/mkdocs_autorefs/plugin.py index 57b441a..ff8d3ae 100644 --- a/src/mkdocs_autorefs/plugin.py +++ b/src/mkdocs_autorefs/plugin.py @@ -302,7 +302,8 @@ def on_post_page(self, output: str, page: Page, **kwargs: Any) -> str: # noqa: fixed_output, unmapped = fix_refs(output, url_mapper, _legacy_refs=self.legacy_refs) if unmapped and log.isEnabledFor(logging.WARNING): - for ref in unmapped: - log.warning(f"{page.file.src_path}: Could not find cross-reference target '[{ref}]'") + for ref, context in unmapped: + message = f"from {context.filepath}:{context.lineno}: ({context.origin}) " if context else "" + log.warning(f"{page.file.src_path}: {message}Could not find cross-reference target '{ref}'") return fixed_output diff --git a/src/mkdocs_autorefs/references.py b/src/mkdocs_autorefs/references.py index 7c361d1..f116310 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -5,6 +5,8 @@ import logging import re import warnings +from abc import ABC, abstractmethod +from dataclasses import dataclass from functools import lru_cache from html import escape, unescape from html.parser import HTMLParser @@ -20,6 +22,8 @@ from markdown.util import HTML_PLACEHOLDER_RE, INLINE_PLACEHOLDER_RE if TYPE_CHECKING: + from pathlib import Path + from markdown import Markdown from mkdocs_autorefs.plugin import AutorefsPlugin @@ -59,10 +63,56 @@ def __getattr__(name: str) -> Any: """ +class AutorefsHookInterface(ABC): + """An interface for hooking into how AutoRef handles inline references.""" + + @dataclass + class Context: + """The context around an auto-reference.""" + + domain: str + role: str + origin: str + filepath: str | Path + lineno: int + + def as_dict(self) -> dict[str, str]: + """Convert the context to a dictionary of HTML attributes.""" + return { + "domain": self.domain, + "role": self.role, + "origin": self.origin, + "filepath": str(self.filepath), + "lineno": str(self.lineno), + } + + @abstractmethod + def expand_identifier(self, identifier: str) -> str: + """Expand an identifier in a given context. + + Parameters: + identifier: The identifier to expand. + + Returns: + The expanded identifier. + """ + raise NotImplementedError + + @abstractmethod + def get_context(self) -> AutorefsHookInterface.Context: + """Get the current context. + + Returns: + The current context. + """ + raise NotImplementedError + + class AutorefsInlineProcessor(ReferenceInlineProcessor): """A Markdown extension to handle inline references.""" name: str = "mkdocs-autorefs" + hook: AutorefsHookInterface | None = None def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 super().__init__(REFERENCE_RE, *args, **kwargs) @@ -145,6 +195,9 @@ def _make_tag(self, identifier: str, text: str) -> Element: A new element. """ el = Element("autoref") + if self.hook: + identifier = self.hook.expand_identifier(identifier) + el.attrib.update(self.hook.get_context().as_dict()) el.set("identifier", identifier) el.text = text return el @@ -177,7 +230,10 @@ def relative_url(url_a: str, url_b: str) -> str: # YORE: Bump 2: Remove block. -def _legacy_fix_ref(url_mapper: Callable[[str], str], unmapped: list[str]) -> Callable: +def _legacy_fix_ref( + url_mapper: Callable[[str], str], + unmapped: list[tuple[str, AutorefsHookInterface.Context | None]], +) -> 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. @@ -210,7 +266,7 @@ def inner(match: Match) -> str: return title if kind == "autorefs-optional-hover": return f'{title}' - unmapped.append(identifier) + unmapped.append((identifier, None)) if title == identifier: return f"[{identifier}][]" return f"[{title}][{identifier}]" @@ -233,7 +289,30 @@ def inner(match: Match) -> str: class _AutorefsAttrs(dict): - _handled_attrs: ClassVar[set[str]] = {"identifier", "optional", "hover", "class"} + _handled_attrs: ClassVar[set[str]] = { + "identifier", + "optional", + "hover", + "class", + "domain", + "role", + "origin", + "filepath", + "lineno", + } + + @property + def context(self) -> AutorefsHookInterface.Context | None: + try: + return AutorefsHookInterface.Context( + domain=self["domain"], + role=self["role"], + origin=self["origin"], + filepath=self["filepath"], + lineno=int(self["lineno"]), + ) + except KeyError: + return None @property def remaining(self) -> str: @@ -257,7 +336,10 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None _html_attrs_parser = _HTMLAttrsParser() -def fix_ref(url_mapper: Callable[[str], str], unmapped: list[str]) -> Callable: +def fix_ref( + url_mapper: Callable[[str], str], + unmapped: list[tuple[str, AutorefsHookInterface.Context | None]], +) -> 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. @@ -290,7 +372,7 @@ def inner(match: Match) -> str: if hover: return f'{title}' return title - unmapped.append(identifier) + unmapped.append((identifier, attrs.context)) if title == identifier: return f"[{identifier}][]" return f"[{title}][{identifier}]" @@ -310,7 +392,12 @@ def inner(match: Match) -> str: # YORE: Bump 2: Replace `, *, _legacy_refs: bool = True` with `` within line. -def fix_refs(html: str, url_mapper: Callable[[str], str], *, _legacy_refs: bool = True) -> tuple[str, list[str]]: +def fix_refs( + html: str, + url_mapper: Callable[[str], str], + *, + _legacy_refs: bool = True, +) -> tuple[str, list[tuple[str, AutorefsHookInterface.Context | None]]]: """Fix all references in the given HTML text. Arguments: @@ -319,9 +406,9 @@ def fix_refs(html: str, url_mapper: Callable[[str], str], *, _legacy_refs: bool such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][]. Returns: - The fixed HTML. + The fixed HTML, and a list of unmapped identifiers (string and optional context). """ - unmapped: list[str] = [] + unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] = [] html = AUTOREF_RE.sub(fix_ref(url_mapper, unmapped), html) # YORE: Bump 2: Remove block. diff --git a/tests/test_references.py b/tests/test_references.py index 3eab1f0..6fdf24e 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -9,7 +9,7 @@ import pytest from mkdocs_autorefs.plugin import AutorefsPlugin -from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url +from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url @pytest.mark.parametrize( @@ -46,7 +46,7 @@ def run_references_test( url_map: dict[str, str], source: str, output: str, - unmapped: list[str] | None = None, + unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None, from_url: str = "page.html", extensions: Mapping = {}, ) -> None: @@ -169,7 +169,7 @@ def test_missing_reference() -> None: url_map={"NotFoo": "foo.html#NotFoo"}, source="[Foo][]", output="

[Foo][]

", - unmapped=["Foo"], + unmapped=[("Foo", None)], ) @@ -179,7 +179,7 @@ def test_missing_reference_with_markdown_text() -> None: url_map={"NotFoo": "foo.html#NotFoo"}, source="[`Foo`][Foo]", output="

[Foo][Foo]

", - unmapped=["Foo"], + unmapped=[("Foo", None)], ) @@ -189,7 +189,7 @@ def test_missing_reference_with_markdown_id() -> None: url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"}, source="[Foo][*NotFoo*]", output="

[Foo][*NotFoo*]

", - unmapped=["*NotFoo*"], + unmapped=[("*NotFoo*", None)], ) @@ -199,7 +199,7 @@ def test_missing_reference_with_markdown_implicit() -> None: url_map={"Foo-bar": "foo.html#Foo-bar"}, source="[*Foo-bar*][] and [`Foo`-bar][]", output="

[Foo-bar][*Foo-bar*] and [Foo-bar][]

", - unmapped=["*Foo-bar*"], + unmapped=[("*Foo-bar*", None)], ) @@ -224,7 +224,7 @@ def test_legacy_custom_required_reference() -> None: with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): output, unmapped = fix_refs(source, url_map.__getitem__) assert output == '[foo][bar] ok' - assert unmapped == ["bar"] + assert unmapped == [("bar", None)] def test_custom_required_reference() -> None: @@ -233,7 +233,7 @@ def test_custom_required_reference() -> None: source = "foo ok" output, unmapped = fix_refs(source, url_map.__getitem__) assert output == '[foo][bar] ok' - assert unmapped == ["bar"] + assert unmapped == [("bar", None)] def test_legacy_custom_optional_reference() -> None: