From fe3c22f4921a54c7331795e838f57dc64516efd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 14 May 2024 16:36:04 +0200 Subject: [PATCH 1/3] feat: Provide hook interface, use it to expand identifiers and attach additional context to references --- src/mkdocs_autorefs/references.py | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/mkdocs_autorefs/references.py b/src/mkdocs_autorefs/references.py index 2fe1d6a..ea72339 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -4,7 +4,10 @@ import logging import re +from abc import ABC, abstractmethod +from dataclasses import dataclass from html import escape, unescape +from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, Match from urllib.parse import urlsplit from xml.etree.ElementTree import Element @@ -40,10 +43,53 @@ """ +class AutoRefHookInterface(ABC): + """An interface for hooking into how AutoRef handles inline references.""" + + @dataclass + class Context: + domain: str + role: str + origin: str + filepath: str | Path + lineno: int + + def as_dict(self) -> dict[str, str]: + return { + "data-autorefs-domain": self.domain, + "data-autorefs-role": self.role, + "data-autorefs-origin": self.origin, + "data-autorefs-filepath": str(self.filepath), + "data-autorefs-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) -> AutoRefHookInterface.Context: + """Get the current context. + + Returns: + The current context. + """ + raise NotImplementedError + + class AutoRefInlineProcessor(ReferenceInlineProcessor): """A Markdown extension.""" name: str = "mkdocs-autorefs" + hook: AutoRefHookInterface | None = None def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 super().__init__(REFERENCE_RE, *args, **kwargs) @@ -127,6 +173,9 @@ def _make_tag(self, identifier: str, text: str) -> Element: A new element. """ el = Element("span") + if self.hook: + identifier = self.hook.expand_identifier(identifier) + el.attrib.update(self.hook.get_context().as_dict()) el.set("data-autorefs-identifier", identifier) el.text = text return el From aad4ed3ab825bf9bcda4a02eba5ce49b663bc9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 24 May 2024 20:26:18 +0200 Subject: [PATCH 2/3] fixup! feat: Provide hook interface, use it to expand identifiers and attach additional context to references --- src/mkdocs_autorefs/references.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/mkdocs_autorefs/references.py b/src/mkdocs_autorefs/references.py index 489b21b..bc72c81 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -9,7 +9,6 @@ from dataclasses import dataclass from html import escape, unescape from html.parser import HTMLParser -from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, Match from urllib.parse import urlsplit from xml.etree.ElementTree import Element @@ -22,6 +21,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 @@ -58,11 +59,13 @@ def __getattr__(name: str) -> Any: """ -class AutoRefHookInterface(ABC): +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 @@ -70,12 +73,13 @@ class Context: lineno: int def as_dict(self) -> dict[str, str]: + """Convert the context to a dictionary of HTML attributes.""" return { - "data-autorefs-domain": self.domain, - "data-autorefs-role": self.role, - "data-autorefs-origin": self.origin, - "data-autorefs-filepath": str(self.filepath), - "data-autorefs-lineno": str(self.lineno), + "domain": self.domain, + "role": self.role, + "origin": self.origin, + "filepath": str(self.filepath), + "lineno": str(self.lineno), } @abstractmethod @@ -91,7 +95,7 @@ def expand_identifier(self, identifier: str) -> str: raise NotImplementedError @abstractmethod - def get_context(self) -> AutoRefHookInterface.Context: + def get_context(self) -> AutorefsHookInterface.Context: """Get the current context. Returns: @@ -104,7 +108,7 @@ class AutorefsInlineProcessor(ReferenceInlineProcessor): """A Markdown extension to handle inline references.""" name: str = "mkdocs-autorefs" - hook: AutoRefHookInterface | None = None + hook: AutorefsHookInterface | None = None def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 super().__init__(REFERENCE_RE, *args, **kwargs) From 2498f0e7dcac8eec059c6822c8fc392927727e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 1 Sep 2024 19:11:49 +0200 Subject: [PATCH 3/3] fixup! feat: Provide hook interface, use it to expand identifiers and attach additional context to references --- src/mkdocs_autorefs/plugin.py | 5 ++-- src/mkdocs_autorefs/references.py | 50 ++++++++++++++++++++++++++----- tests/test_references.py | 16 +++++----- 3 files changed, 53 insertions(+), 18 deletions(-) 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 f0f7723..f116310 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -230,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. @@ -263,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}]" @@ -286,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: @@ -310,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. @@ -343,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}]" @@ -363,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: @@ -372,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: