Skip to content

Commit

Permalink
refactor: Store parent pages *and parent sections* in backlink breadc…
Browse files Browse the repository at this point in the history
…rumbs

Previously, backlink breadcrumbs would only store pages, now they also store the sections within the leaf page, providing the full navigation path.
  • Loading branch information
pawamoy committed Mar 8, 2025
1 parent 3ac4797 commit 67955ce
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 34 deletions.
13 changes: 10 additions & 3 deletions src/mkdocs_autorefs/_internal/backlinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@
_log = logging.getLogger(f"mkdocs.plugins.{__name__}") # type: ignore[assignment]


@dataclass(eq=True, frozen=True, order=True)
@dataclass(frozen=True, order=True)
class BacklinkCrumb:
"""A navigation breadcrumb for a backlink."""

title: str
"""The title of the page."""
"""The title of the breadcrumb."""
url: str
"""The URL of the page."""
"""The URL of the breadcrumb."""
parent: BacklinkCrumb | None = None
"""The parent breadcrumb."""

def __eq__(self, value: object) -> bool:
if isinstance(value, BacklinkCrumb):
return self.url == value.url
return False


@dataclass(eq=True, frozen=True, order=True)
Expand Down
67 changes: 48 additions & 19 deletions src/mkdocs_autorefs/_internal/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def __init__(self) -> None:
self._primary_url_map: dict[str, list[str]] = {}
self._secondary_url_map: dict[str, list[str]] = {}
self._title_map: dict[str, str] = {}
self._backlink_page_map: dict[str, Page] = {}
self._breadcrumbs_map: dict[str, BacklinkCrumb] = {}
self._abs_url_map: dict[str, str] = {}
self._backlinks: dict[str, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set))
# YORE: Bump 2: Remove line.
Expand Down Expand Up @@ -299,6 +299,7 @@ def on_env(self, env: Environment, /, *, config: MkDocsConfig, files: Files) ->
# ----------------------------------------------------------------------- #
# Utilities #
# ----------------------------------------------------------------------- #
# TODO: Maybe stop exposing this method in the future.
def map_urls(self, page: Page, anchor: AnchorLink) -> None:
"""Recurse on every anchor to map its ID to its absolute URL.
Expand All @@ -308,6 +309,9 @@ def map_urls(self, page: Page, anchor: AnchorLink) -> None:
page: The page containing the anchors.
anchor: The anchor to process and to recurse on.
"""
return self._map_urls(page, anchor)

def _map_urls(self, page: Page, anchor: AnchorLink, parent: BacklinkCrumb | None = None) -> None:
# YORE: Bump 2: Remove block.
if isinstance(page, str):
try:
Expand All @@ -316,8 +320,36 @@ def map_urls(self, page: Page, anchor: AnchorLink) -> None:
page = self.current_page

self.register_anchor(page, anchor.id, title=anchor.title, primary=True)
breadcrumb = self._get_breadcrumb(page, anchor, parent)
for child in anchor.children:
self.map_urls(page, child)
self._map_urls(page, child, breadcrumb)

def _get_breadcrumb(
self,
page: Page | Section,
anchor: AnchorLink | None = None,
parent: BacklinkCrumb | None = None,
) -> BacklinkCrumb:
parent_breadcrumb = None if page.parent is None else self._get_breadcrumb(page.parent)
if parent is None:
if isinstance(page, Page):
if (parent_url := page.url) not in self._breadcrumbs_map:
self._breadcrumbs_map[parent_url] = BacklinkCrumb(
title=page.title,
url=parent_url,
parent=parent_breadcrumb,
)
parent = self._breadcrumbs_map[parent_url]
else:
parent = BacklinkCrumb(title=page.title, url="", parent=parent_breadcrumb)
if anchor is None:
return parent
if (url := f"{page.url}#{anchor.id}") not in self._breadcrumbs_map: # type: ignore[union-attr]
# Skip the parent page if the anchor is a top-level heading, to reduce repetition.
if anchor.level == 1:
parent = parent.parent
self._breadcrumbs_map[url] = BacklinkCrumb(title=anchor.title, url=url, parent=parent)
return self._breadcrumbs_map[url]

def _record_backlink(self, identifier: str, backlink_type: str, backlink_anchor: str, page_url: str) -> None:
"""Record a backlink.
Expand Down Expand Up @@ -351,23 +383,22 @@ def get_backlinks(self, *identifiers: str, from_url: str) -> dict[str, set[Backl
backlinks = self._backlinks.get(identifier, {})
for backlink_type, backlink_urls in backlinks.items():
for backlink_url in backlink_urls:
relative_backlinks[backlink_type].add(self._crumbs(from_url, backlink_url))
relative_backlinks[backlink_type].add(self._get_backlink(from_url, backlink_url))
return relative_backlinks

def _crumbs(self, from_url: str, backlink_url: str) -> Backlink:
backlink_page: Page = self._backlink_page_map[backlink_url]
backlink_title = self._title_map.get(backlink_url, "")
crumbs: list[BacklinkCrumb] = [
BacklinkCrumb(backlink_title, relative_url(from_url, backlink_url)),
BacklinkCrumb(backlink_page.title, relative_url(from_url, backlink_page.url + "#")),
]
page: Page | Section = backlink_page
while page.parent:
page = page.parent
if url := getattr(page, "url", ""):
url = relative_url(from_url, url + "#")
crumbs.append(BacklinkCrumb(page.title, url))
return Backlink(tuple(reversed(crumbs)))
def _get_backlink(self, from_url: str, backlink_url: str) -> Backlink:
breadcrumbs = []
breadcrumb: BacklinkCrumb | None = self._breadcrumbs_map[backlink_url]
while breadcrumb:
breadcrumbs.append(
BacklinkCrumb(
title=breadcrumb.title,
url=breadcrumb.url and relative_url(from_url, breadcrumb.url),
parent=breadcrumb.parent,
),
)
breadcrumb = breadcrumb.parent
return Backlink(tuple(reversed(breadcrumbs)))

def register_anchor(
self,
Expand Down Expand Up @@ -403,8 +434,6 @@ def register_anchor(
url_map[identifier] = [url]
if title and url not in self._title_map:
self._title_map[url] = title
if self.record_backlinks and url not in self._backlink_page_map:
self._backlink_page_map[url] = page

def register_url(self, identifier: str, url: str) -> None:
"""Register that the identifier should be turned into a link to this URL.
Expand Down
10 changes: 10 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from mkdocs.structure.toc import AnchorLink


def create_page(url: str) -> Page:
Expand All @@ -12,3 +13,12 @@ def create_page(url: str) -> Page:
file=File(url, "docs", "site", use_directory_urls=False),
config=MkDocsConfig(),
)


def create_anchor_link(title: str, anchor_id: str, level: int = 1) -> AnchorLink:
"""Create an anchor link."""
return AnchorLink(
title=title,
id=anchor_id,
level=level,
)
18 changes: 6 additions & 12 deletions tests/test_backlinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from mkdocs_autorefs import AUTOREF_RE, AutorefsExtension, AutorefsPlugin, Backlink, BacklinkCrumb
from mkdocs_autorefs._internal.references import _html_attrs_parser
from tests.helpers import create_page
from tests.helpers import create_anchor_link, create_page


def test_record_backlinks() -> None:
Expand All @@ -26,17 +26,11 @@ def test_get_backlinks() -> None:
"""Check that backlinks can be retrieved."""
plugin = AutorefsPlugin()
plugin.record_backlinks = True
plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=True)
plugin._record_backlink("foo", "referenced-by", "foo", "foo.html")
assert plugin.get_backlinks("foo", from_url="") == {
"referenced-by": {
Backlink(
crumbs=(
BacklinkCrumb(title="foo.html", url="foo.html#"),
BacklinkCrumb(title="", url="foo.html#foo"),
),
),
},
plugin.map_urls(create_page("foo.html"), create_anchor_link("Foo", "foo"))
plugin._primary_url_map["bar"] = ["bar.html#bar"]
plugin._record_backlink("bar", "referenced-by", "foo", "foo.html")
assert plugin.get_backlinks("bar", from_url="") == {
"referenced-by": {Backlink(crumbs=(BacklinkCrumb(title="Foo", url="foo.html#foo", parent=None),))},
}


Expand Down

0 comments on commit 67955ce

Please sign in to comment.