@@ -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.
0 commit comments