Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Provide hook interface, use it to expand identifiers and attach additional context to references #46

Merged
merged 5 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
pawamoy marked this conversation as resolved.
Show resolved Hide resolved
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
Loading