13
13
from __future__ import annotations
14
14
15
15
import contextlib
16
- from dataclasses import dataclass
17
16
import functools
18
17
import logging
19
18
from collections import defaultdict
19
+ from dataclasses import dataclass
20
20
from pathlib import PurePosixPath as URL # noqa: N814
21
21
from typing import TYPE_CHECKING , Any , Callable
22
22
from urllib .parse import urlsplit
26
26
from mkdocs .config .config_options import Type
27
27
from mkdocs .plugins import BasePlugin , event_priority
28
28
from mkdocs .structure .pages import Page
29
- from mkdocs .structure .files import Files
30
- from mkdocs .structure .nav import Section
31
- from jinja2 .environment import Environment
32
29
33
- from mkdocs_autorefs .references import AutorefsExtension , URLAndTitle , _find_backlinks , fix_refs , relative_url
30
+ from mkdocs_autorefs .references import AutorefsExtension , URLAndTitle , fix_refs , relative_url
34
31
35
32
if TYPE_CHECKING :
36
33
from collections .abc import Sequence
37
34
35
+ from jinja2 .environment import Environment
38
36
from mkdocs .config .defaults import MkDocsConfig
37
+ from mkdocs .structure .files import Files
38
+ from mkdocs .structure .nav import Section
39
39
from mkdocs .structure .pages import Page
40
40
from mkdocs .structure .toc import AnchorLink
41
41
48
48
log = logging .getLogger (f"mkdocs.plugins.{ __name__ } " ) # type: ignore[assignment]
49
49
50
50
51
- @dataclass (order = True )
51
+ @dataclass (eq = True , frozen = True , order = True )
52
52
class BacklinkCrumb :
53
+ """A navigation breadcrumb for a backlink."""
54
+
53
55
title : str
54
56
url : str
55
57
56
58
57
- @dataclass (order = True )
59
+ @dataclass (eq = True , frozen = True , order = True )
58
60
class Backlink :
59
- crumbs : list [BacklinkCrumb ]
61
+ """A backlink (list of breadcrumbs)."""
62
+
63
+ crumbs : tuple [BacklinkCrumb , ...]
60
64
61
65
62
66
class AutorefsConfig (Config ):
@@ -77,6 +81,17 @@ class AutorefsConfig(Config):
77
81
When false and multiple URLs are found for an identifier, autorefs will log a warning and resolve to the first URL.
78
82
"""
79
83
84
+ link_titles = Type (bool , default = True )
85
+ """Whether to set titles on links.
86
+
87
+ When true, and when the link's text is different from the linked object's full id,
88
+ autorefs will set the title HTML attribute on the link, using the linked object's full id.
89
+ Such title attributes are displayed as tooltips when hovering over the links.
90
+
91
+ The presence of titles prevents the instant preview feature of Material for MkDocs from working,
92
+ so it might be useful to disable this option when using this Material for MkDocs feature.
93
+ """
94
+
80
95
81
96
class AutorefsPlugin (BasePlugin [AutorefsConfig ]):
82
97
"""The `autorefs` plugin for `mkdocs`.
@@ -164,26 +179,26 @@ def _record_backlink(self, identifier: str, backlink_type: str, backlink_anchor:
164
179
if identifier in self ._primary_url_map or identifier in self ._secondary_url_map :
165
180
self ._backlinks [identifier ][backlink_type ].add (f"{ page_url } #{ backlink_anchor } " )
166
181
167
- def get_backlinks (self , * identifiers : str , from_url : str ) -> dict [str , list [Backlink ]]:
182
+ def get_backlinks (self , * identifiers : str , from_url : str ) -> dict [str , set [Backlink ]]:
168
183
"""Return the backlinks to an identifier relative to the given URL.
169
184
170
185
Arguments:
171
186
*identifiers: The identifiers to get backlinks for.
172
187
from_url: The URL of the page where backlinks are rendered.
173
188
174
189
Returns:
175
- A dictionary of backlinks, with the type of reference as key and a list of backlinks as balue .
176
- Each backlink is a list of (URL, title) tuples forming navigation breadcrumbs in reverse .
190
+ A dictionary of backlinks, with the type of reference as key and a set of backlinks as value .
191
+ Each backlink is a tuple of (URL, title) tuples forming navigation breadcrumbs.
177
192
"""
178
- relative_backlinks : dict [str , list [Backlink ]] = defaultdict (list )
179
- for identifier in identifiers :
193
+ relative_backlinks : dict [str , set [Backlink ]] = defaultdict (set )
194
+ for identifier in set ( identifiers ) :
180
195
backlinks = self ._backlinks .get (identifier , {})
181
196
for backlink_type , backlink_urls in backlinks .items ():
182
197
for backlink_url in backlink_urls :
183
- relative_backlinks [backlink_type ].append (self ._nav (from_url , backlink_url ))
198
+ relative_backlinks [backlink_type ].add (self ._crumbs (from_url , backlink_url ))
184
199
return relative_backlinks
185
200
186
- def _nav (self , from_url : str , backlink_url : str ) -> Backlink :
201
+ def _crumbs (self , from_url : str , backlink_url : str ) -> Backlink :
187
202
backlink_page : Page = self ._backlink_page_map [backlink_url ]
188
203
backlink_title = self ._title_map [backlink_url ]
189
204
crumbs : list [BacklinkCrumb ] = [
@@ -196,10 +211,11 @@ def _nav(self, from_url: str, backlink_url: str) -> Backlink:
196
211
if url := getattr (page , "url" , "" ):
197
212
url = relative_url (from_url , url + "#" )
198
213
crumbs .append (BacklinkCrumb (page .title , url ))
199
- return Backlink (crumbs )
214
+ return Backlink (tuple ( reversed ( crumbs )) )
200
215
201
216
def register_anchor (
202
217
self ,
218
+ page : Page ,
203
219
identifier : str ,
204
220
anchor : str | None = None ,
205
221
* ,
@@ -209,12 +225,12 @@ def register_anchor(
209
225
"""Register that an anchor corresponding to an identifier was encountered when rendering the page.
210
226
211
227
Arguments:
228
+ page: The page where the anchor was found.
212
229
identifier: The identifier to register.
213
230
anchor: The anchor on the page, without `#`. If not provided, defaults to the identifier.
214
231
title: The title of the anchor (optional).
215
232
primary: Whether this anchor is the primary one for the identifier.
216
233
"""
217
- page : Page = self .current_page # type: ignore[assignment]
218
234
url = f"{ page .url } #{ anchor or identifier } "
219
235
url_map = self ._primary_url_map if primary else self ._secondary_url_map
220
236
if identifier in url_map :
@@ -389,23 +405,24 @@ def on_page_content(self, html: str, page: Page, **kwargs: Any) -> str: # noqa:
389
405
if self .scan_toc :
390
406
log .debug ("Mapping identifiers to URLs for page %s" , page .file .src_path )
391
407
for item in page .toc .items :
392
- self .map_urls (item )
408
+ self .map_urls (page , item )
393
409
return html
394
410
395
- def map_urls (self , anchor : AnchorLink ) -> None :
411
+ def map_urls (self , page : Page , anchor : AnchorLink ) -> None :
396
412
"""Recurse on every anchor to map its ID to its absolute URL.
397
413
398
414
This method populates `self._primary_url_map` by side-effect.
399
415
400
416
Arguments:
417
+ page: The page containing the anchors.
401
418
anchor: The anchor to process and to recurse on.
402
419
"""
403
- self .register_anchor (anchor .id , title = anchor .title , primary = True )
420
+ self .register_anchor (page , anchor .id , title = anchor .title , primary = True )
404
421
for child in anchor .children :
405
- self .map_urls (child )
422
+ self .map_urls (page , child )
406
423
407
424
@event_priority (- 50 ) # Late, after mkdocstrings has finished loading inventories.
408
- def on_env (self , env : Environment , / , * , config : MkDocsConfig , files : Files ) -> Environment :
425
+ def on_env (self , env : Environment , / , * , config : MkDocsConfig , files : Files ) -> Environment : # noqa: ARG002
409
426
"""Apply cross-references and collect backlinks.
410
427
411
428
Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
@@ -431,14 +448,26 @@ def on_env(self, env: Environment, /, *, config: MkDocsConfig, files: Files) ->
431
448
log .debug ("Applying cross-refs in page %s" , file .page .file .src_path )
432
449
433
450
# YORE: Bump 2: Replace `, fallback=self.get_fallback_anchor` with `` within line.
434
- url_mapper = functools .partial (self .get_item_url , from_url = file .page .url , fallback = self .get_fallback_anchor )
451
+ url_mapper = functools .partial (
452
+ self .get_item_url ,
453
+ from_url = file .page .url ,
454
+ fallback = self .get_fallback_anchor ,
455
+ )
435
456
backlink_recorder = functools .partial (self ._record_backlink , page_url = file .page .url )
436
457
# YORE: Bump 2: Replace `, _legacy_refs=self.legacy_refs` with `` within line.
437
- file .page .content , unmapped = fix_refs (file .page .content , url_mapper , record_backlink = backlink_recorder , _legacy_refs = self .legacy_refs )
458
+ file .page .content , unmapped = fix_refs (
459
+ file .page .content ,
460
+ url_mapper ,
461
+ record_backlink = backlink_recorder ,
462
+ link_titles = self .config .link_titles ,
463
+ _legacy_refs = self .legacy_refs ,
464
+ )
438
465
439
466
if unmapped and log .isEnabledFor (logging .WARNING ):
440
467
for ref , context in unmapped :
441
468
message = f"from { context .filepath } :{ context .lineno } : ({ context .origin } ) " if context else ""
442
- log .warning (f"{ file .page .file .src_path } : { message } Could not find cross-reference target '{ ref } '" )
469
+ log .warning (
470
+ f"{ file .page .file .src_path } : { message } Could not find cross-reference target '{ ref } '" ,
471
+ )
443
472
444
473
return env
0 commit comments