Skip to content

Commit fb8df98

Browse files
authored
feat: Provide hook interface, use it to expand identifiers, attach additional context to references, and give more context around unmapped identifers
Issue-54: #54 PR-mkdocstrings#666: mkdocstrings/mkdocstrings#666
1 parent b36a0d1 commit fb8df98

File tree

3 files changed

+106
-18
lines changed

3 files changed

+106
-18
lines changed

src/mkdocs_autorefs/plugin.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def on_post_page(self, output: str, page: Page, **kwargs: Any) -> str: # noqa:
302302
fixed_output, unmapped = fix_refs(output, url_mapper, _legacy_refs=self.legacy_refs)
303303

304304
if unmapped and log.isEnabledFor(logging.WARNING):
305-
for ref in unmapped:
306-
log.warning(f"{page.file.src_path}: Could not find cross-reference target '[{ref}]'")
305+
for ref, context in unmapped:
306+
message = f"from {context.filepath}:{context.lineno}: ({context.origin}) " if context else ""
307+
log.warning(f"{page.file.src_path}: {message}Could not find cross-reference target '{ref}'")
307308

308309
return fixed_output

src/mkdocs_autorefs/references.py

+95-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import logging
66
import re
77
import warnings
8+
from abc import ABC, abstractmethod
9+
from dataclasses import dataclass
810
from functools import lru_cache
911
from html import escape, unescape
1012
from html.parser import HTMLParser
@@ -20,6 +22,8 @@
2022
from markdown.util import HTML_PLACEHOLDER_RE, INLINE_PLACEHOLDER_RE
2123

2224
if TYPE_CHECKING:
25+
from pathlib import Path
26+
2327
from markdown import Markdown
2428

2529
from mkdocs_autorefs.plugin import AutorefsPlugin
@@ -59,10 +63,56 @@ def __getattr__(name: str) -> Any:
5963
"""
6064

6165

66+
class AutorefsHookInterface(ABC):
67+
"""An interface for hooking into how AutoRef handles inline references."""
68+
69+
@dataclass
70+
class Context:
71+
"""The context around an auto-reference."""
72+
73+
domain: str
74+
role: str
75+
origin: str
76+
filepath: str | Path
77+
lineno: int
78+
79+
def as_dict(self) -> dict[str, str]:
80+
"""Convert the context to a dictionary of HTML attributes."""
81+
return {
82+
"domain": self.domain,
83+
"role": self.role,
84+
"origin": self.origin,
85+
"filepath": str(self.filepath),
86+
"lineno": str(self.lineno),
87+
}
88+
89+
@abstractmethod
90+
def expand_identifier(self, identifier: str) -> str:
91+
"""Expand an identifier in a given context.
92+
93+
Parameters:
94+
identifier: The identifier to expand.
95+
96+
Returns:
97+
The expanded identifier.
98+
"""
99+
raise NotImplementedError
100+
101+
@abstractmethod
102+
def get_context(self) -> AutorefsHookInterface.Context:
103+
"""Get the current context.
104+
105+
Returns:
106+
The current context.
107+
"""
108+
raise NotImplementedError
109+
110+
62111
class AutorefsInlineProcessor(ReferenceInlineProcessor):
63112
"""A Markdown extension to handle inline references."""
64113

65114
name: str = "mkdocs-autorefs"
115+
hook: AutorefsHookInterface | None = None
66116

67117
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
68118
super().__init__(REFERENCE_RE, *args, **kwargs)
@@ -145,6 +195,9 @@ def _make_tag(self, identifier: str, text: str) -> Element:
145195
A new element.
146196
"""
147197
el = Element("autoref")
198+
if self.hook:
199+
identifier = self.hook.expand_identifier(identifier)
200+
el.attrib.update(self.hook.get_context().as_dict())
148201
el.set("identifier", identifier)
149202
el.text = text
150203
return el
@@ -177,7 +230,10 @@ def relative_url(url_a: str, url_b: str) -> str:
177230

178231

179232
# YORE: Bump 2: Remove block.
180-
def _legacy_fix_ref(url_mapper: Callable[[str], str], unmapped: list[str]) -> Callable:
233+
def _legacy_fix_ref(
234+
url_mapper: Callable[[str], str],
235+
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]],
236+
) -> Callable:
181237
"""Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).
182238
183239
In our context, we match Markdown references and replace them with HTML links.
@@ -210,7 +266,7 @@ def inner(match: Match) -> str:
210266
return title
211267
if kind == "autorefs-optional-hover":
212268
return f'<span title="{identifier}">{title}</span>'
213-
unmapped.append(identifier)
269+
unmapped.append((identifier, None))
214270
if title == identifier:
215271
return f"[{identifier}][]"
216272
return f"[{title}][{identifier}]"
@@ -233,7 +289,30 @@ def inner(match: Match) -> str:
233289

234290

235291
class _AutorefsAttrs(dict):
236-
_handled_attrs: ClassVar[set[str]] = {"identifier", "optional", "hover", "class"}
292+
_handled_attrs: ClassVar[set[str]] = {
293+
"identifier",
294+
"optional",
295+
"hover",
296+
"class",
297+
"domain",
298+
"role",
299+
"origin",
300+
"filepath",
301+
"lineno",
302+
}
303+
304+
@property
305+
def context(self) -> AutorefsHookInterface.Context | None:
306+
try:
307+
return AutorefsHookInterface.Context(
308+
domain=self["domain"],
309+
role=self["role"],
310+
origin=self["origin"],
311+
filepath=self["filepath"],
312+
lineno=int(self["lineno"]),
313+
)
314+
except KeyError:
315+
return None
237316

238317
@property
239318
def remaining(self) -> str:
@@ -257,7 +336,10 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None
257336
_html_attrs_parser = _HTMLAttrsParser()
258337

259338

260-
def fix_ref(url_mapper: Callable[[str], str], unmapped: list[str]) -> Callable:
339+
def fix_ref(
340+
url_mapper: Callable[[str], str],
341+
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]],
342+
) -> Callable:
261343
"""Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).
262344
263345
In our context, we match Markdown references and replace them with HTML links.
@@ -290,7 +372,7 @@ def inner(match: Match) -> str:
290372
if hover:
291373
return f'<span title="{identifier}">{title}</span>'
292374
return title
293-
unmapped.append(identifier)
375+
unmapped.append((identifier, attrs.context))
294376
if title == identifier:
295377
return f"[{identifier}][]"
296378
return f"[{title}][{identifier}]"
@@ -310,7 +392,12 @@ def inner(match: Match) -> str:
310392

311393

312394
# YORE: Bump 2: Replace `, *, _legacy_refs: bool = True` with `` within line.
313-
def fix_refs(html: str, url_mapper: Callable[[str], str], *, _legacy_refs: bool = True) -> tuple[str, list[str]]:
395+
def fix_refs(
396+
html: str,
397+
url_mapper: Callable[[str], str],
398+
*,
399+
_legacy_refs: bool = True,
400+
) -> tuple[str, list[tuple[str, AutorefsHookInterface.Context | None]]]:
314401
"""Fix all references in the given HTML text.
315402
316403
Arguments:
@@ -319,9 +406,9 @@ def fix_refs(html: str, url_mapper: Callable[[str], str], *, _legacy_refs: bool
319406
such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
320407
321408
Returns:
322-
The fixed HTML.
409+
The fixed HTML, and a list of unmapped identifiers (string and optional context).
323410
"""
324-
unmapped: list[str] = []
411+
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] = []
325412
html = AUTOREF_RE.sub(fix_ref(url_mapper, unmapped), html)
326413

327414
# YORE: Bump 2: Remove block.

tests/test_references.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pytest
1010

1111
from mkdocs_autorefs.plugin import AutorefsPlugin
12-
from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url
12+
from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url
1313

1414

1515
@pytest.mark.parametrize(
@@ -46,7 +46,7 @@ def run_references_test(
4646
url_map: dict[str, str],
4747
source: str,
4848
output: str,
49-
unmapped: list[str] | None = None,
49+
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None,
5050
from_url: str = "page.html",
5151
extensions: Mapping = {},
5252
) -> None:
@@ -169,7 +169,7 @@ def test_missing_reference() -> None:
169169
url_map={"NotFoo": "foo.html#NotFoo"},
170170
source="[Foo][]",
171171
output="<p>[Foo][]</p>",
172-
unmapped=["Foo"],
172+
unmapped=[("Foo", None)],
173173
)
174174

175175

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

185185

@@ -189,7 +189,7 @@ def test_missing_reference_with_markdown_id() -> None:
189189
url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"},
190190
source="[Foo][*NotFoo*]",
191191
output="<p>[Foo][*NotFoo*]</p>",
192-
unmapped=["*NotFoo*"],
192+
unmapped=[("*NotFoo*", None)],
193193
)
194194

195195

@@ -199,7 +199,7 @@ def test_missing_reference_with_markdown_implicit() -> None:
199199
url_map={"Foo-bar": "foo.html#Foo-bar"},
200200
source="[*Foo-bar*][] and [`Foo`-bar][]",
201201
output="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][]</p>",
202-
unmapped=["*Foo-bar*"],
202+
unmapped=[("*Foo-bar*", None)],
203203
)
204204

205205

@@ -224,7 +224,7 @@ def test_legacy_custom_required_reference() -> None:
224224
with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"):
225225
output, unmapped = fix_refs(source, url_map.__getitem__)
226226
assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>'
227-
assert unmapped == ["bar"]
227+
assert unmapped == [("bar", None)]
228228

229229

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

238238

239239
def test_legacy_custom_optional_reference() -> None:

0 commit comments

Comments
 (0)