Skip to content

Commit 67955ce

Browse files
committed
refactor: Store parent pages *and parent sections* in backlink breadcrumbs
Previously, backlink breadcrumbs would only store pages, now they also store the sections within the leaf page, providing the full navigation path.
1 parent 3ac4797 commit 67955ce

File tree

4 files changed

+74
-34
lines changed

4 files changed

+74
-34
lines changed

src/mkdocs_autorefs/_internal/backlinks.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,21 @@
2525
_log = logging.getLogger(f"mkdocs.plugins.{__name__}") # type: ignore[assignment]
2626

2727

28-
@dataclass(eq=True, frozen=True, order=True)
28+
@dataclass(frozen=True, order=True)
2929
class BacklinkCrumb:
3030
"""A navigation breadcrumb for a backlink."""
3131

3232
title: str
33-
"""The title of the page."""
33+
"""The title of the breadcrumb."""
3434
url: str
35-
"""The URL of the page."""
35+
"""The URL of the breadcrumb."""
36+
parent: BacklinkCrumb | None = None
37+
"""The parent breadcrumb."""
38+
39+
def __eq__(self, value: object) -> bool:
40+
if isinstance(value, BacklinkCrumb):
41+
return self.url == value.url
42+
return False
3643

3744

3845
@dataclass(eq=True, frozen=True, order=True)

src/mkdocs_autorefs/_internal/plugin.py

+48-19
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def __init__(self) -> None:
141141
self._primary_url_map: dict[str, list[str]] = {}
142142
self._secondary_url_map: dict[str, list[str]] = {}
143143
self._title_map: dict[str, str] = {}
144-
self._backlink_page_map: dict[str, Page] = {}
144+
self._breadcrumbs_map: dict[str, BacklinkCrumb] = {}
145145
self._abs_url_map: dict[str, str] = {}
146146
self._backlinks: dict[str, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set))
147147
# YORE: Bump 2: Remove line.
@@ -299,6 +299,7 @@ def on_env(self, env: Environment, /, *, config: MkDocsConfig, files: Files) ->
299299
# ----------------------------------------------------------------------- #
300300
# Utilities #
301301
# ----------------------------------------------------------------------- #
302+
# TODO: Maybe stop exposing this method in the future.
302303
def map_urls(self, page: Page, anchor: AnchorLink) -> None:
303304
"""Recurse on every anchor to map its ID to its absolute URL.
304305
@@ -308,6 +309,9 @@ def map_urls(self, page: Page, anchor: AnchorLink) -> None:
308309
page: The page containing the anchors.
309310
anchor: The anchor to process and to recurse on.
310311
"""
312+
return self._map_urls(page, anchor)
313+
314+
def _map_urls(self, page: Page, anchor: AnchorLink, parent: BacklinkCrumb | None = None) -> None:
311315
# YORE: Bump 2: Remove block.
312316
if isinstance(page, str):
313317
try:
@@ -316,8 +320,36 @@ def map_urls(self, page: Page, anchor: AnchorLink) -> None:
316320
page = self.current_page
317321

318322
self.register_anchor(page, anchor.id, title=anchor.title, primary=True)
323+
breadcrumb = self._get_breadcrumb(page, anchor, parent)
319324
for child in anchor.children:
320-
self.map_urls(page, child)
325+
self._map_urls(page, child, breadcrumb)
326+
327+
def _get_breadcrumb(
328+
self,
329+
page: Page | Section,
330+
anchor: AnchorLink | None = None,
331+
parent: BacklinkCrumb | None = None,
332+
) -> BacklinkCrumb:
333+
parent_breadcrumb = None if page.parent is None else self._get_breadcrumb(page.parent)
334+
if parent is None:
335+
if isinstance(page, Page):
336+
if (parent_url := page.url) not in self._breadcrumbs_map:
337+
self._breadcrumbs_map[parent_url] = BacklinkCrumb(
338+
title=page.title,
339+
url=parent_url,
340+
parent=parent_breadcrumb,
341+
)
342+
parent = self._breadcrumbs_map[parent_url]
343+
else:
344+
parent = BacklinkCrumb(title=page.title, url="", parent=parent_breadcrumb)
345+
if anchor is None:
346+
return parent
347+
if (url := f"{page.url}#{anchor.id}") not in self._breadcrumbs_map: # type: ignore[union-attr]
348+
# Skip the parent page if the anchor is a top-level heading, to reduce repetition.
349+
if anchor.level == 1:
350+
parent = parent.parent
351+
self._breadcrumbs_map[url] = BacklinkCrumb(title=anchor.title, url=url, parent=parent)
352+
return self._breadcrumbs_map[url]
321353

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

357-
def _crumbs(self, from_url: str, backlink_url: str) -> Backlink:
358-
backlink_page: Page = self._backlink_page_map[backlink_url]
359-
backlink_title = self._title_map.get(backlink_url, "")
360-
crumbs: list[BacklinkCrumb] = [
361-
BacklinkCrumb(backlink_title, relative_url(from_url, backlink_url)),
362-
BacklinkCrumb(backlink_page.title, relative_url(from_url, backlink_page.url + "#")),
363-
]
364-
page: Page | Section = backlink_page
365-
while page.parent:
366-
page = page.parent
367-
if url := getattr(page, "url", ""):
368-
url = relative_url(from_url, url + "#")
369-
crumbs.append(BacklinkCrumb(page.title, url))
370-
return Backlink(tuple(reversed(crumbs)))
389+
def _get_backlink(self, from_url: str, backlink_url: str) -> Backlink:
390+
breadcrumbs = []
391+
breadcrumb: BacklinkCrumb | None = self._breadcrumbs_map[backlink_url]
392+
while breadcrumb:
393+
breadcrumbs.append(
394+
BacklinkCrumb(
395+
title=breadcrumb.title,
396+
url=breadcrumb.url and relative_url(from_url, breadcrumb.url),
397+
parent=breadcrumb.parent,
398+
),
399+
)
400+
breadcrumb = breadcrumb.parent
401+
return Backlink(tuple(reversed(breadcrumbs)))
371402

372403
def register_anchor(
373404
self,
@@ -403,8 +434,6 @@ def register_anchor(
403434
url_map[identifier] = [url]
404435
if title and url not in self._title_map:
405436
self._title_map[url] = title
406-
if self.record_backlinks and url not in self._backlink_page_map:
407-
self._backlink_page_map[url] = page
408437

409438
def register_url(self, identifier: str, url: str) -> None:
410439
"""Register that the identifier should be turned into a link to this URL.

tests/helpers.py

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from mkdocs.config.defaults import MkDocsConfig
44
from mkdocs.structure.files import File
55
from mkdocs.structure.pages import Page
6+
from mkdocs.structure.toc import AnchorLink
67

78

89
def create_page(url: str) -> Page:
@@ -12,3 +13,12 @@ def create_page(url: str) -> Page:
1213
file=File(url, "docs", "site", use_directory_urls=False),
1314
config=MkDocsConfig(),
1415
)
16+
17+
18+
def create_anchor_link(title: str, anchor_id: str, level: int = 1) -> AnchorLink:
19+
"""Create an anchor link."""
20+
return AnchorLink(
21+
title=title,
22+
id=anchor_id,
23+
level=level,
24+
)

tests/test_backlinks.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from mkdocs_autorefs import AUTOREF_RE, AutorefsExtension, AutorefsPlugin, Backlink, BacklinkCrumb
1010
from mkdocs_autorefs._internal.references import _html_attrs_parser
11-
from tests.helpers import create_page
11+
from tests.helpers import create_anchor_link, create_page
1212

1313

1414
def test_record_backlinks() -> None:
@@ -26,17 +26,11 @@ def test_get_backlinks() -> None:
2626
"""Check that backlinks can be retrieved."""
2727
plugin = AutorefsPlugin()
2828
plugin.record_backlinks = True
29-
plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=True)
30-
plugin._record_backlink("foo", "referenced-by", "foo", "foo.html")
31-
assert plugin.get_backlinks("foo", from_url="") == {
32-
"referenced-by": {
33-
Backlink(
34-
crumbs=(
35-
BacklinkCrumb(title="foo.html", url="foo.html#"),
36-
BacklinkCrumb(title="", url="foo.html#foo"),
37-
),
38-
),
39-
},
29+
plugin.map_urls(create_page("foo.html"), create_anchor_link("Foo", "foo"))
30+
plugin._primary_url_map["bar"] = ["bar.html#bar"]
31+
plugin._record_backlink("bar", "referenced-by", "foo", "foo.html")
32+
assert plugin.get_backlinks("bar", from_url="") == {
33+
"referenced-by": {Backlink(crumbs=(BacklinkCrumb(title="Foo", url="foo.html#foo", parent=None),))},
4034
}
4135

4236

0 commit comments

Comments
 (0)