Skip to content

Commit

Permalink
feat: Provide hook interface, use it to expand identifiers, attach ad…
Browse files Browse the repository at this point in the history
…ditional context to references, and give more context around unmapped identifers

Issue-54: #54
PR-mkdocstrings#666: mkdocstrings/mkdocstrings#666
  • Loading branch information
pawamoy authored Sep 1, 2024
1 parent b36a0d1 commit fb8df98
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 18 deletions.
5 changes: 3 additions & 2 deletions src/mkdocs_autorefs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
103 changes: 95 additions & 8 deletions src/mkdocs_autorefs/references.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -210,7 +266,7 @@ def inner(match: Match) -> str:
return title
if kind == "autorefs-optional-hover":
return f'<span title="{identifier}">{title}</span>'
unmapped.append(identifier)
unmapped.append((identifier, None))
if title == identifier:
return f"[{identifier}][]"
return f"[{title}][{identifier}]"
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -290,7 +372,7 @@ def inner(match: Match) -> str:
if hover:
return f'<span title="{identifier}">{title}</span>'
return title
unmapped.append(identifier)
unmapped.append((identifier, attrs.context))
if title == identifier:
return f"[{identifier}][]"
return f"[{title}][{identifier}]"
Expand All @@ -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:
Expand All @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions tests/test_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -169,7 +169,7 @@ def test_missing_reference() -> None:
url_map={"NotFoo": "foo.html#NotFoo"},
source="[Foo][]",
output="<p>[Foo][]</p>",
unmapped=["Foo"],
unmapped=[("Foo", None)],
)


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


Expand All @@ -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="<p>[Foo][*NotFoo*]</p>",
unmapped=["*NotFoo*"],
unmapped=[("*NotFoo*", None)],
)


Expand All @@ -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="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][]</p>",
unmapped=["*Foo-bar*"],
unmapped=[("*Foo-bar*", None)],
)


Expand All @@ -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] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
assert unmapped == ["bar"]
assert unmapped == [("bar", None)]


def test_custom_required_reference() -> None:
Expand All @@ -233,7 +233,7 @@ def test_custom_required_reference() -> None:
source = "<autoref identifier=bar>foo</autoref> <autoref identifier=ok>ok</autoref>"
output, unmapped = fix_refs(source, url_map.__getitem__)
assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
assert unmapped == ["bar"]
assert unmapped == [("bar", None)]


def test_legacy_custom_optional_reference() -> None:
Expand Down

0 comments on commit fb8df98

Please sign in to comment.