From 71c976cd9661cecf42ca766ae8a858da2f4d5f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 24 May 2026 19:10:00 +0200 Subject: [PATCH] refactor: allow configuring mkdocstrings as a Markdown extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- python/zensical/compat/mkdocstrings.py | 26 +++++--- python/zensical/config.py | 14 ++-- python/zensical/extensions/mkdocstrings.py | 76 ++++++++++++++++++++++ python/zensical/markdown/render.py | 5 +- 4 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 python/zensical/extensions/mkdocstrings.py diff --git a/python/zensical/compat/mkdocstrings.py b/python/zensical/compat/mkdocstrings.py index 55ef5dd..d93f079 100644 --- a/python/zensical/compat/mkdocstrings.py +++ b/python/zensical/compat/mkdocstrings.py @@ -23,6 +23,7 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING, Any from zensical.extensions.autorefs import get_autorefs_store @@ -60,8 +61,13 @@ class ToolConfig: def get_mkdocstrings_extension( + handlers: dict[str, Any] | None = None, + *, + custom_templates: str | None = None, + enable_inventory: bool = True, # noqa: ARG001 + default_handler: str = "python", + locale: str = "en", config: dict[str, Any], - path: str, ) -> MkdocstringsExtension: """Create the mkdocstrings Markdown extension.""" from mkdocstrings import ( # noqa: PLC0415 # ty:ignore[unresolved-import] @@ -73,19 +79,19 @@ def get_mkdocstrings_extension( global HANDLERS # noqa: PLW0603 if HANDLERS is None: - mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"] - tool_config = ToolConfig(config_file_path=path) + root_dir = Path(config["root_dir"]) + config_file = root_dir / "zensical.toml" + tool_config = ToolConfig(config_file_path=str(config_file)) HANDLERS = Handlers( theme="material", - default=mkdocstrings_config.get("default_handler") or "python", - inventory_project=mkdocstrings_config.get("inventory_project") - or config["site_name"], - inventory_version=mkdocstrings_config.get("inventory_version"), - handlers_config=mkdocstrings_config.get("handlers"), - custom_templates=mkdocstrings_config.get("custom_templates"), + default=default_handler, + inventory_project=config["site_name"], + inventory_version="0.0.0", + handlers_config=handlers if handlers is not None else {}, + custom_templates=custom_templates, mdx=config["markdown_extensions"], mdx_config=config["mdx_configs"], - locale=mkdocstrings_config.get("locale"), + locale=locale, tool_config=tool_config, ) diff --git a/python/zensical/config.py b/python/zensical/config.py index ec0bcbd..cf1dea5 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -41,11 +41,11 @@ from tomli import load as toml_load from yaml import Loader, YAMLError from yaml.constructor import ConstructorError -from zensical.compat.mkdocstrings import get_mkdocstrings_extension from zensical.extensions.autorefs import AutorefsExtension from zensical.extensions.emoji import to_svg, twemoji from zensical.extensions.glightbox import GlightboxExtension from zensical.extensions.macros import MacrosExtension +from zensical.extensions.mkdocstrings import MkdocstringsExtension if TYPE_CHECKING: from collections.abc import Iterator @@ -679,7 +679,7 @@ def _apply_defaults(config: dict, path: str) -> dict: # Map plugins configuration to Markdown extensions _shim_autorefs(config) _shim_markdown_exec(config) - _shim_mkdocstrings(config, path) + _shim_mkdocstrings(config) _shim_glightbox(config) _shim_macros(config) @@ -858,19 +858,19 @@ def _shim_markdown_exec(config: dict[str, Any]) -> None: ) -def _shim_mkdocstrings(config: dict[str, Any], path: str) -> None: +def _shim_mkdocstrings(config: dict[str, Any]) -> None: # Map mkdocstrings plugin configuration to the extension configuration if "mkdocstrings" in config["plugins"]: - mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"] - if mkdocstrings_config.get("enabled", True): + plugin = config["plugins"]["mkdocstrings"]["config"] + if plugin.get("enabled", True): if not find_spec("mkdocstrings"): raise ConfigurationError( "mkdocstrings plugin is enabled, but mkdocstrings is not " "installed. Please install mkdocstrings or disable the " "plugin." ) - mkdocstrings = get_mkdocstrings_extension(config, path) - config["markdown_extensions"].append(mkdocstrings) + config["markdown_extensions"].append(MkdocstringsExtension.name) + config["mdx_configs"][MkdocstringsExtension.name] = plugin def _shim_glightbox(config: dict[str, Any]) -> None: diff --git a/python/zensical/extensions/mkdocstrings.py b/python/zensical/extensions/mkdocstrings.py new file mode 100644 index 0000000..7a17661 --- /dev/null +++ b/python/zensical/extensions/mkdocstrings.py @@ -0,0 +1,76 @@ +# Copyright (c) 2025-2026 Zensical and contributors + +# SPDX-License-Identifier: MIT +# All contributions are certified under the DCO + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from markdown import Extension, Markdown + +from zensical.compat.mkdocstrings import get_mkdocstrings_extension +from zensical.extensions.context import ContextPreprocessor + +if TYPE_CHECKING: + from markdown import Markdown + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + + +class MkdocstringsExtension(Extension): + """Markdown extension that renders Python API documentation.""" + + name = "zensical.extensions.mkdocstrings" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the extension.""" + self._enabled: bool = kwargs.pop("enabled", True) + self._kwargs = kwargs + + def extendMarkdown(self, md: Markdown) -> None: + """Register Markdown extension.""" + if not self._enabled: + return + # The context extension must have registered its dummy processor + # by then. We ensure this in `render()` by inserting it first + # in the list of extension so that Python-Markdown calls its + # `extendMarkdown()` method first. + if context := ContextPreprocessor.from_markdown(md): + config = context.config + # We need the following indirection because we have + # to pass the configured Markdown extensions again + # to the actual extension. + ext = get_mkdocstrings_extension(**self._kwargs, config=config) + ext.extendMarkdown(md) + + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + + +def makeExtension(**kwargs: Any) -> MkdocstringsExtension: + """Register Markdown extension.""" + return MkdocstringsExtension(**kwargs) diff --git a/python/zensical/markdown/render.py b/python/zensical/markdown/render.py index 0a148b6..713d532 100644 --- a/python/zensical/markdown/render.py +++ b/python/zensical/markdown/render.py @@ -96,11 +96,12 @@ def render(content: str, path: str, url: str) -> dict: extension._kwargs["page"] = page break else: - config["markdown_extensions"].append( + config["markdown_extensions"].insert( + 0, ContextExtension( page=page, config=config, - ) + ), ) # Initialize Markdown parser