diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e3659e..826873b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.29.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.29.0) - 2025-03-10 + +[Compare with 0.28.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.3...0.29.0) + +**This is the last version before v1!** + +### Build + +- Depend on MkDocs 1.6 ([11bc400](https://github.com/mkdocstrings/mkdocstrings/commit/11bc400ab7089a47755f24a790c08f2f904c570b) by Timothée Mazzucotelli). + +### Features + +- Support rendering backlinks through handlers ([d4c7b9c](https://github.com/mkdocstrings/mkdocstrings/commit/d4c7b9c42f2de5df234c1ffefae0405a120e383c) by Timothée Mazzucotelli). [Issue-723](https://github.com/mkdocstrings/mkdocstrings/issues/723), [Issue-mkdocstrings-python-153](https://github.com/mkdocstrings/python/issues/153), [PR-739](https://github.com/mkdocstrings/mkdocstrings/pull/739) + +### Code Refactoring + +- Save and forward titles to autorefs ([f49fb29](https://github.com/mkdocstrings/mkdocstrings/commit/f49fb29582714795ca03febf1ee243aa2992917e) by Timothée Mazzucotelli). +- Use a combined event (each split with a different priority) for `on_env` ([8d1dd75](https://github.com/mkdocstrings/mkdocstrings/commit/8d1dd754b4babd3c4f9e6c1d8856be57fe4ba9ea) by Timothée Mazzucotelli). + ## [0.28.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.3) - 2025-03-08 [Compare with 0.28.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.2...0.28.3) diff --git a/pyproject.toml b/pyproject.toml index 07dfa630..c3087f61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,8 @@ dependencies = [ "Jinja2>=2.11.1", "Markdown>=3.6", "MarkupSafe>=1.1", - "mkdocs>=1.4", + "mkdocs>=1.6", "mkdocs-autorefs>=1.4", - "mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5. "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", "typing-extensions>=4.1; python_version < '3.10'", diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 35ceca41..182fc563 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -240,54 +240,56 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: # If we were in an inner handler layer, we wouldn't do any of this # and would just let headings bubble up to the outer handler layer. - page = self._autorefs.current_page - if page is not None: - for heading in headings: - rendered_id = heading.attrib["id"] - self._autorefs.register_anchor(page, rendered_id, primary=True) - - # Register all identifiers for this object - # both in the autorefs plugin and in the inventory. - aliases: tuple[str, ...] - # YORE: Bump 1: Replace block with line 16. - if hasattr(handler, "get_anchors"): - warn( - "The `get_anchors` method is deprecated. " - "Declare a `get_aliases` method instead, accepting a string (identifier) " - "instead of a collected object.", - DeprecationWarning, - stacklevel=1, - ) - try: - data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {})) - except CollectionError: - aliases = () - else: - aliases = handler.get_anchors(data_object) + if (page := self._autorefs.current_page) is None: + return + + for heading in headings: + rendered_id = heading.attrib["id"] + # The title is registered to be used as tooltip by autorefs. + self._autorefs.register_anchor(page, rendered_id, title=heading.text, primary=True) + + # Register all identifiers for this object + # both in the autorefs plugin and in the inventory. + aliases: tuple[str, ...] + # YORE: Bump 1: Replace block with line 16. + if hasattr(handler, "get_anchors"): + warn( + "The `get_anchors` method is deprecated. " + "Declare a `get_aliases` method instead, accepting a string (identifier) " + "instead of a collected object.", + DeprecationWarning, + stacklevel=1, + ) + try: + data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {})) + except CollectionError: + aliases = () else: - aliases = handler.get_aliases(rendered_id) - + aliases = handler.get_anchors(data_object) + else: + aliases = handler.get_aliases(rendered_id) + + for alias in aliases: + if alias != rendered_id: + self._autorefs.register_anchor(page, alias, rendered_id, primary=False) + + if "data-role" in heading.attrib: + self._handlers.inventory.register( + name=rendered_id, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=1, # Register with standard priority. + uri=f"{page.url}#{rendered_id}", + ) for alias in aliases: - if alias != rendered_id: - self._autorefs.register_anchor(page, alias, rendered_id, primary=False) - - if "data-role" in heading.attrib: - self._handlers.inventory.register( - name=rendered_id, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=1, # Register with standard priority. - uri=f"{page}#{rendered_id}", - ) - for alias in aliases: - if alias not in self._handlers.inventory: - self._handlers.inventory.register( - name=alias, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=2, # Register with lower priority. - uri=f"{page}#{rendered_id}", - ) + if alias not in self._handlers.inventory: + self._handlers.inventory.register( + name=alias, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=2, # Register with lower priority. + uri=f"{page.url}#{rendered_id}", + ) class _HeadingsPostProcessor(Treeprocessor): diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 32a4576e..7784f007 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -19,10 +19,8 @@ from markdown import Markdown from markdown.extensions.toc import TocTreeprocessor from markupsafe import Markup -from mkdocs_autorefs import AutorefsInlineProcessor - -# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. -from mkdocs_get_deps.cache import download_and_cache_url +from mkdocs.utils.cache import download_and_cache_url +from mkdocs_autorefs import AutorefsInlineProcessor, BacklinksTreeProcessor from mkdocstrings._internal.download import _download_url_with_gz from mkdocstrings._internal.handlers.rendering import ( @@ -45,7 +43,7 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from markdown import Extension - from mkdocs_autorefs import AutorefsHookInterface + from mkdocs_autorefs import AutorefsHookInterface, Backlink _logger = get_logger(__name__) @@ -333,6 +331,10 @@ def render(self, data: CollectorItem, options: HandlerOptions) -> str: """ raise NotImplementedError + def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: ARG002 + """Render backlinks.""" + return "" + def teardown(self) -> None: """Teardown the handler. @@ -411,7 +413,7 @@ def do_convert_markdown( text: The text to convert. heading_level: The base heading level to start all Markdown headings from. html_id: The HTML id of the element that's considered the parent of this element. - strip_paragraph: Whether to exclude the
tag from around the whole output. + strip_paragraph: Whether to exclude the `
` tag from around the whole output. Returns: An HTML string. @@ -422,6 +424,8 @@ def do_convert_markdown( treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level # type: ignore[attr-defined] treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--" # type: ignore[attr-defined] treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph # type: ignore[attr-defined] + if BacklinksTreeProcessor.name in treeprocessors: + treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id # type: ignore[attr-defined] if autoref_hook: self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined] @@ -432,6 +436,8 @@ def do_convert_markdown( treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0 # type: ignore[attr-defined] treeprocessors[IdPrependingTreeprocessor.name].id_prefix = "" # type: ignore[attr-defined] treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined] + if BacklinksTreeProcessor.name in treeprocessors: + treeprocessors[BacklinksTreeProcessor.name].initial_id = None # type: ignore[attr-defined] self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] self.md.reset() _markdown_conversion_layer -= 1 @@ -475,6 +481,8 @@ def do_heading( el.set("data-toc-label", toc_label) if role: el.set("data-role", role) + if content: + el.text = str(content).strip() self._headings.append(el) if hidden: diff --git a/src/mkdocstrings/_internal/handlers/rendering.py b/src/mkdocstrings/_internal/handlers/rendering.py index 23b444ec..25db87e1 100644 --- a/src/mkdocstrings/_internal/handlers/rendering.py +++ b/src/mkdocstrings/_internal/handlers/rendering.py @@ -252,7 +252,7 @@ def run(self, root: Element) -> None: class ParagraphStrippingTreeprocessor(Treeprocessor): - """Unwraps the
element around the whole output.""" + """Unwraps the `
` element around the whole output.""" name: str = "mkdocstrings_strip_paragraph" """The name of the treeprocessor.""" @@ -263,7 +263,7 @@ class ParagraphStrippingTreeprocessor(Treeprocessor): def run(self, root: Element) -> Element | None: """Unwrap the root element if it's a single `
` element.""" if self.strip and len(root) == 1 and root[0].tag == "p": - # Turn the single
element into the root element and inherit its tag name (it's significant!) + # Turn the single `
` element into the root element and inherit its tag name (it's significant!)
root[0].tag = root.tag
return root[0]
return None
diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py
index 7cb5b9ad..d7adf1c6 100644
--- a/src/mkdocstrings/_internal/plugin.py
+++ b/src/mkdocstrings/_internal/plugin.py
@@ -14,13 +14,14 @@
from __future__ import annotations
import os
-import sys
+import re
+from re import Match
from typing import TYPE_CHECKING, Any
from warnings import catch_warnings, simplefilter
from mkdocs.config import Config
from mkdocs.config import config_options as opt
-from mkdocs.plugins import BasePlugin
+from mkdocs.plugins import BasePlugin, CombinedEvent, event_priority
from mkdocs.utils import write_file
from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin
@@ -28,14 +29,10 @@
from mkdocstrings._internal.handlers.base import BaseHandler, Handlers
from mkdocstrings._internal.loggers import get_logger
-if sys.version_info < (3, 10):
- pass
-else:
- pass
-
if TYPE_CHECKING:
from jinja2.environment import Environment
from mkdocs.config.defaults import MkDocsConfig
+ from mkdocs.structure.files import Files
_logger = get_logger(__name__)
@@ -148,6 +145,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
handlers._download_inventories()
+ AutorefsPlugin.record_backlinks = True
autorefs: AutorefsPlugin
try:
# If autorefs plugin is explicitly enabled, just use it.
@@ -170,6 +168,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files.
+ self._autorefs = autorefs
self._handlers = handlers
return config
@@ -194,29 +193,65 @@ def plugin_enabled(self) -> bool:
"""
return self.config.enabled
- def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
- """Extra actions that need to happen after all Markdown rendering and before HTML rendering.
-
- Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env).
-
- - Write mkdocstrings' extra files into the site dir.
- - Gather results from background inventory download tasks.
- """
- if not self.plugin_enabled:
- return
+ @event_priority(50) # Early, before autorefs' starts applying cross-refs and collecting backlinks.
+ def _on_env_load_inventories(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
+ if self.plugin_enabled and self._handlers:
+ register = config.plugins["autorefs"].register_url # type: ignore[attr-defined]
+ for identifier, url in self._handlers._yield_inventory_items():
+ register(identifier, url)
- if self._handlers:
+ @event_priority(-20) # Late, not important.
+ def _on_env_add_css(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
+ if self.plugin_enabled and self._handlers:
css_content = "\n".join(handler.extra_css for handler in self.handlers.seen_handlers)
write_file(css_content.encode("utf-8"), os.path.join(config.site_dir, self.css_filename))
- if self.inventory_enabled:
- _logger.debug("Creating inventory file objects.inv")
- inv_contents = self.handlers.inventory.format_sphinx()
- write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
+ @event_priority(-20) # Late, not important.
+ def _on_env_write_inventory(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
+ if self.plugin_enabled and self._handlers and self.inventory_enabled:
+ _logger.debug("Creating inventory file objects.inv")
+ inv_contents = self.handlers.inventory.format_sphinx()
+ write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
- register = config.plugins["autorefs"].register_url # type: ignore[attr-defined]
- for identifier, url in self._handlers._yield_inventory_items():
- register(identifier, url)
+ @event_priority(-100) # Last, after autorefs has finished applying cross-refs and collecting backlinks.
+ def _on_env_apply_backlinks(self, env: Environment, /, *, config: MkDocsConfig, files: Files) -> Environment: # noqa: ARG002
+ regex = re.compile(r"