Skip to content

Commit dd69005

Browse files
committed
feat: Support rendering backlinks through handlers
Handlers must add `backlinks` HTML elements to their templates: ```html <backlinks identifier="some-id" handler="handler-name" /> ``` mkdocstrings will run a regular expression substitution on each page's HTML, and call corresponding handlers' `render_backlinks` method with backlinks fetched from autorefs (using the specified identifier, and aliases obtained thanks to it through the same handler). Issue-723: #723 Issue-mkdocstrings-python-153: mkdocstrings/python#153 PR-739: #739
1 parent 959e0c5 commit dd69005

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

src/mkdocstrings/handlers/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from markdown.extensions.toc import TocTreeprocessor
2222
from markupsafe import Markup
2323
from mkdocs.utils.cache import download_and_cache_url
24-
from mkdocs_autorefs import AutorefsInlineProcessor
24+
from mkdocs_autorefs import AutorefsInlineProcessor, BacklinksTreeProcessor
2525

2626
from mkdocstrings._download import download_url_with_gz
2727
from mkdocstrings.handlers.rendering import (
@@ -44,7 +44,7 @@
4444
from collections.abc import Iterable, Iterator, Mapping, Sequence
4545

4646
from markdown import Extension
47-
from mkdocs_autorefs import AutorefsHookInterface
47+
from mkdocs_autorefs import AutorefsHookInterface, Backlink
4848

4949
log = get_logger(__name__)
5050

@@ -323,6 +323,10 @@ def render(self, data: CollectorItem, options: HandlerOptions) -> str:
323323
"""
324324
raise NotImplementedError
325325

326+
def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: ARG002
327+
"""Render backlinks."""
328+
return ""
329+
326330
def teardown(self) -> None:
327331
"""Teardown the handler.
328332
@@ -412,6 +416,8 @@ def do_convert_markdown(
412416
treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level # type: ignore[attr-defined]
413417
treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--" # type: ignore[attr-defined]
414418
treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph # type: ignore[attr-defined]
419+
if BacklinksTreeProcessor.name in treeprocessors:
420+
treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id # type: ignore[attr-defined]
415421

416422
if autoref_hook:
417423
self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined]
@@ -422,6 +428,8 @@ def do_convert_markdown(
422428
treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0 # type: ignore[attr-defined]
423429
treeprocessors[IdPrependingTreeprocessor.name].id_prefix = "" # type: ignore[attr-defined]
424430
treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined]
431+
if BacklinksTreeProcessor.name in treeprocessors:
432+
treeprocessors[BacklinksTreeProcessor.name].initial_id = None # type: ignore[attr-defined]
425433
self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined]
426434
self.md.reset()
427435
_markdown_conversion_layer -= 1

src/mkdocstrings/plugin.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from __future__ import annotations
1616

1717
import os
18+
import re
1819
import sys
1920
from collections.abc import Iterable, Mapping
21+
from re import Match
2022
from typing import TYPE_CHECKING, Any, Callable, TypeVar
2123
from warnings import catch_warnings, simplefilter
2224

@@ -38,6 +40,7 @@
3840
if TYPE_CHECKING:
3941
from jinja2.environment import Environment
4042
from mkdocs.config.defaults import MkDocsConfig
43+
from mkdocs.structure.files import Files
4144

4245

4346
log = get_logger(__name__)
@@ -234,13 +237,44 @@ def _on_env_write_inventory(self, env: Environment, config: MkDocsConfig, *args:
234237
inv_contents = self.handlers.inventory.format_sphinx()
235238
write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
236239

237-
on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory)
240+
@event_priority(-100) # Last, after autorefs has finished applying cross-refs and collecting backlinks.
241+
def _on_env_apply_backlinks(self, env: Environment, /, *, config: MkDocsConfig, files: Files) -> Environment: # noqa: ARG002
242+
regex = re.compile(r"<backlinks\s+identifier=\"([^\"]+)\"\s+handler=\"([^\"]+)\"\s*/?>")
243+
244+
def repl(match: Match) -> str:
245+
handler_name = match.group(2)
246+
handler = self.handlers.get_handler(handler_name)
247+
248+
# The handler doesn't implement backlinks,
249+
# return early to avoid computing them.
250+
if handler.render_backlinks.__func__ is BaseHandler.render_backlinks: # type: ignore[attr-defined]
251+
return ""
252+
253+
identifier = match.group(1)
254+
aliases = handler.get_aliases(identifier)
255+
backlinks = self._autorefs.get_backlinks(identifier, *aliases, from_url=file.page.url) # type: ignore[union-attr]
256+
257+
# No backlinks, avoid calling the handler's method.
258+
if not backlinks:
259+
return ""
260+
261+
return handler.render_backlinks(backlinks)
262+
263+
for file in files:
264+
if file.page and file.page.content:
265+
log.debug("Applying backlinks in page %s", file.page.file.src_path)
266+
file.page.content = regex.sub(repl, file.page.content)
267+
268+
return env
269+
270+
on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory, _on_env_apply_backlinks)
238271
"""Extra actions that need to happen after all Markdown-to-HTML page rendering.
239272
240273
Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
241274
242275
- Gather results from background inventory download tasks.
243276
- Write mkdocstrings' extra files (CSS, inventory) into the site directory.
277+
- Apply backlinks to the HTML output of each page.
244278
"""
245279

246280
def on_post_build(

0 commit comments

Comments
 (0)