Skip to content

Commit 44462d0

Browse files
committed
Merge branch 'main' of github.com:mkdocstrings/mkdocstrings
2 parents 3366f5b + d4c7b9c commit 44462d0

4 files changed

Lines changed: 122 additions & 78 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ dependencies = [
3434
"Jinja2>=2.11.1",
3535
"Markdown>=3.6",
3636
"MarkupSafe>=1.1",
37-
"mkdocs>=1.4",
37+
"mkdocs>=1.6",
3838
"mkdocs-autorefs>=1.4",
39-
"mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5.
4039
"pymdown-extensions>=6.3",
4140
"importlib-metadata>=4.6; python_version < '3.10'",
4241
"typing-extensions>=4.1; python_version < '3.10'",

src/mkdocstrings/_internal/extension.py

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -240,54 +240,56 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None:
240240
# If we were in an inner handler layer, we wouldn't do any of this
241241
# and would just let headings bubble up to the outer handler layer.
242242

243-
page = self._autorefs.current_page
244-
if page is not None:
245-
for heading in headings:
246-
rendered_id = heading.attrib["id"]
247-
self._autorefs.register_anchor(page, rendered_id, primary=True)
248-
249-
# Register all identifiers for this object
250-
# both in the autorefs plugin and in the inventory.
251-
aliases: tuple[str, ...]
252-
# YORE: Bump 1: Replace block with line 16.
253-
if hasattr(handler, "get_anchors"):
254-
warn(
255-
"The `get_anchors` method is deprecated. "
256-
"Declare a `get_aliases` method instead, accepting a string (identifier) "
257-
"instead of a collected object.",
258-
DeprecationWarning,
259-
stacklevel=1,
260-
)
261-
try:
262-
data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {}))
263-
except CollectionError:
264-
aliases = ()
265-
else:
266-
aliases = handler.get_anchors(data_object)
243+
if (page := self._autorefs.current_page) is None:
244+
return
245+
246+
for heading in headings:
247+
rendered_id = heading.attrib["id"]
248+
# The title is registered to be used as tooltip by autorefs.
249+
self._autorefs.register_anchor(page, rendered_id, title=heading.text, primary=True)
250+
251+
# Register all identifiers for this object
252+
# both in the autorefs plugin and in the inventory.
253+
aliases: tuple[str, ...]
254+
# YORE: Bump 1: Replace block with line 16.
255+
if hasattr(handler, "get_anchors"):
256+
warn(
257+
"The `get_anchors` method is deprecated. "
258+
"Declare a `get_aliases` method instead, accepting a string (identifier) "
259+
"instead of a collected object.",
260+
DeprecationWarning,
261+
stacklevel=1,
262+
)
263+
try:
264+
data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {}))
265+
except CollectionError:
266+
aliases = ()
267267
else:
268-
aliases = handler.get_aliases(rendered_id)
269-
268+
aliases = handler.get_anchors(data_object)
269+
else:
270+
aliases = handler.get_aliases(rendered_id)
271+
272+
for alias in aliases:
273+
if alias != rendered_id:
274+
self._autorefs.register_anchor(page, alias, rendered_id, primary=False)
275+
276+
if "data-role" in heading.attrib:
277+
self._handlers.inventory.register(
278+
name=rendered_id,
279+
domain=handler.domain,
280+
role=heading.attrib["data-role"],
281+
priority=1, # Register with standard priority.
282+
uri=f"{page.url}#{rendered_id}",
283+
)
270284
for alias in aliases:
271-
if alias != rendered_id:
272-
self._autorefs.register_anchor(page, alias, rendered_id, primary=False)
273-
274-
if "data-role" in heading.attrib:
275-
self._handlers.inventory.register(
276-
name=rendered_id,
277-
domain=handler.domain,
278-
role=heading.attrib["data-role"],
279-
priority=1, # Register with standard priority.
280-
uri=f"{page}#{rendered_id}",
281-
)
282-
for alias in aliases:
283-
if alias not in self._handlers.inventory:
284-
self._handlers.inventory.register(
285-
name=alias,
286-
domain=handler.domain,
287-
role=heading.attrib["data-role"],
288-
priority=2, # Register with lower priority.
289-
uri=f"{page}#{rendered_id}",
290-
)
285+
if alias not in self._handlers.inventory:
286+
self._handlers.inventory.register(
287+
name=alias,
288+
domain=handler.domain,
289+
role=heading.attrib["data-role"],
290+
priority=2, # Register with lower priority.
291+
uri=f"{page.url}#{rendered_id}",
292+
)
291293

292294

293295
class _HeadingsPostProcessor(Treeprocessor):

src/mkdocstrings/_internal/handlers/base.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@
1919
from markdown import Markdown
2020
from markdown.extensions.toc import TocTreeprocessor
2121
from markupsafe import Markup
22-
from mkdocs_autorefs import AutorefsInlineProcessor
23-
24-
# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5.
25-
from mkdocs_get_deps.cache import download_and_cache_url
22+
from mkdocs.utils.cache import download_and_cache_url
23+
from mkdocs_autorefs import AutorefsInlineProcessor, BacklinksTreeProcessor
2624

2725
from mkdocstrings._internal.download import _download_url_with_gz
2826
from mkdocstrings._internal.handlers.rendering import (
@@ -45,7 +43,7 @@
4543
from collections.abc import Iterable, Iterator, Mapping, Sequence
4644

4745
from markdown import Extension
48-
from mkdocs_autorefs import AutorefsHookInterface
46+
from mkdocs_autorefs import AutorefsHookInterface, Backlink
4947

5048
_logger = get_logger(__name__)
5149

@@ -333,6 +331,10 @@ def render(self, data: CollectorItem, options: HandlerOptions) -> str:
333331
"""
334332
raise NotImplementedError
335333

334+
def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: ARG002
335+
"""Render backlinks."""
336+
return ""
337+
336338
def teardown(self) -> None:
337339
"""Teardown the handler.
338340
@@ -422,6 +424,8 @@ def do_convert_markdown(
422424
treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level # type: ignore[attr-defined]
423425
treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--" # type: ignore[attr-defined]
424426
treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph # type: ignore[attr-defined]
427+
if BacklinksTreeProcessor.name in treeprocessors:
428+
treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id # type: ignore[attr-defined]
425429

426430
if autoref_hook:
427431
self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined]
@@ -432,6 +436,8 @@ def do_convert_markdown(
432436
treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0 # type: ignore[attr-defined]
433437
treeprocessors[IdPrependingTreeprocessor.name].id_prefix = "" # type: ignore[attr-defined]
434438
treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined]
439+
if BacklinksTreeProcessor.name in treeprocessors:
440+
treeprocessors[BacklinksTreeProcessor.name].initial_id = None # type: ignore[attr-defined]
435441
self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined]
436442
self.md.reset()
437443
_markdown_conversion_layer -= 1
@@ -475,6 +481,8 @@ def do_heading(
475481
el.set("data-toc-label", toc_label)
476482
if role:
477483
el.set("data-role", role)
484+
if content:
485+
el.text = str(content).strip()
478486
self._headings.append(el)
479487

480488
if hidden:

src/mkdocstrings/_internal/plugin.py

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,25 @@
1414
from __future__ import annotations
1515

1616
import os
17-
import sys
17+
import re
18+
from re import Match
1819
from typing import TYPE_CHECKING, Any
1920
from warnings import catch_warnings, simplefilter
2021

2122
from mkdocs.config import Config
2223
from mkdocs.config import config_options as opt
23-
from mkdocs.plugins import BasePlugin
24+
from mkdocs.plugins import BasePlugin, CombinedEvent, event_priority
2425
from mkdocs.utils import write_file
2526
from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin
2627

2728
from mkdocstrings._internal.extension import MkdocstringsExtension
2829
from mkdocstrings._internal.handlers.base import BaseHandler, Handlers
2930
from mkdocstrings._internal.loggers import get_logger
3031

31-
if sys.version_info < (3, 10):
32-
pass
33-
else:
34-
pass
35-
3632
if TYPE_CHECKING:
3733
from jinja2.environment import Environment
3834
from mkdocs.config.defaults import MkDocsConfig
35+
from mkdocs.structure.files import Files
3936

4037

4138
_logger = get_logger(__name__)
@@ -148,6 +145,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
148145

149146
handlers._download_inventories()
150147

148+
AutorefsPlugin.record_backlinks = True
151149
autorefs: AutorefsPlugin
152150
try:
153151
# If autorefs plugin is explicitly enabled, just use it.
@@ -170,6 +168,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
170168

171169
config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files.
172170

171+
self._autorefs = autorefs
173172
self._handlers = handlers
174173
return config
175174

@@ -194,29 +193,65 @@ def plugin_enabled(self) -> bool:
194193
"""
195194
return self.config.enabled
196195

197-
def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
198-
"""Extra actions that need to happen after all Markdown rendering and before HTML rendering.
199-
200-
Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
201-
202-
- Write mkdocstrings' extra files into the site dir.
203-
- Gather results from background inventory download tasks.
204-
"""
205-
if not self.plugin_enabled:
206-
return
196+
@event_priority(50) # Early, before autorefs' starts applying cross-refs and collecting backlinks.
197+
def _on_env_load_inventories(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
198+
if self.plugin_enabled and self._handlers:
199+
register = config.plugins["autorefs"].register_url # type: ignore[attr-defined]
200+
for identifier, url in self._handlers._yield_inventory_items():
201+
register(identifier, url)
207202

208-
if self._handlers:
203+
@event_priority(-20) # Late, not important.
204+
def _on_env_add_css(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
205+
if self.plugin_enabled and self._handlers:
209206
css_content = "\n".join(handler.extra_css for handler in self.handlers.seen_handlers)
210207
write_file(css_content.encode("utf-8"), os.path.join(config.site_dir, self.css_filename))
211208

212-
if self.inventory_enabled:
213-
_logger.debug("Creating inventory file objects.inv")
214-
inv_contents = self.handlers.inventory.format_sphinx()
215-
write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
209+
@event_priority(-20) # Late, not important.
210+
def _on_env_write_inventory(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
211+
if self.plugin_enabled and self._handlers and self.inventory_enabled:
212+
_logger.debug("Creating inventory file objects.inv")
213+
inv_contents = self.handlers.inventory.format_sphinx()
214+
write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
216215

217-
register = config.plugins["autorefs"].register_url # type: ignore[attr-defined]
218-
for identifier, url in self._handlers._yield_inventory_items():
219-
register(identifier, url)
216+
@event_priority(-100) # Last, after autorefs has finished applying cross-refs and collecting backlinks.
217+
def _on_env_apply_backlinks(self, env: Environment, /, *, config: MkDocsConfig, files: Files) -> Environment: # noqa: ARG002
218+
regex = re.compile(r"<backlinks\s+identifier=\"([^\"]+)\"\s+handler=\"([^\"]+)\"\s*/?>")
219+
220+
def repl(match: Match) -> str:
221+
handler_name = match.group(2)
222+
handler = self.handlers.get_handler(handler_name)
223+
224+
# The handler doesn't implement backlinks,
225+
# return early to avoid computing them.
226+
if handler.render_backlinks.__func__ is BaseHandler.render_backlinks: # type: ignore[attr-defined]
227+
return ""
228+
229+
identifier = match.group(1)
230+
aliases = handler.get_aliases(identifier)
231+
backlinks = self._autorefs.get_backlinks(identifier, *aliases, from_url=file.page.url) # type: ignore[union-attr]
232+
233+
# No backlinks, avoid calling the handler's method.
234+
if not backlinks:
235+
return ""
236+
237+
return handler.render_backlinks(backlinks)
238+
239+
for file in files:
240+
if file.page and file.page.content:
241+
_logger.debug("Applying backlinks in page %s", file.page.file.src_path)
242+
file.page.content = regex.sub(repl, file.page.content)
243+
244+
return env
245+
246+
on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory, _on_env_apply_backlinks)
247+
"""Extra actions that need to happen after all Markdown-to-HTML page rendering.
248+
249+
Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
250+
251+
- Gather results from background inventory download tasks.
252+
- Write mkdocstrings' extra files (CSS, inventory) into the site directory.
253+
- Apply backlinks to the HTML output of each page.
254+
"""
220255

221256
def on_post_build(
222257
self,

0 commit comments

Comments
 (0)