From 68e0c9845b5abd8d77c8da49e2053241aabcf4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 19 Aug 2026 16:11:56 +0200 Subject: [PATCH] fix: support pymdownx blocks in markdown-exec (#871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- python/zensical/config.py | 25 ++++++++++++++++++------- python/zensical/extensions/context.py | 3 --- python/zensical/extensions/search.py | 5 ----- python/zensical/markdown/render.py | 15 ++++++++++++--- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/python/zensical/config.py b/python/zensical/config.py index b816f4b..05d26b1 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -842,10 +842,18 @@ def _shim_autorefs(config: dict[str, Any]) -> None: def _shim_markdown_exec(config: dict[str, Any]) -> None: # Map markdown-exec plugin configuration to superfences configuration if "markdown-exec" in config["plugins"]: + try: + import markdown_exec # noqa: PLC0415 # ty:ignore[unresolved-import] + except ImportError as error: + raise ConfigurationError( + "markdown-exec plugin is enabled, but markdown-exec is not " + "installed. Please install markdown-exec or disable the plugin." + ) from error markdown_exec_config = config["plugins"]["markdown-exec"]["config"] enabled = markdown_exec_config.pop("enabled", True) languages = markdown_exec_config.get("languages", None) if enabled and (languages or languages is None): + # Check ANSI colors requirement and pygments-ansi-color presence. trueish = ("auto", "required", True) ansi = markdown_exec_config.get("ansi", False) in trueish if ansi and not find_spec("pygments_ansi_color"): @@ -854,6 +862,7 @@ def _shim_markdown_exec(config: dict[str, Any]) -> None: "installed. Please install pygments-ansi-color or turn off " "ANSI requirement in markdown-exec configuration." ) + # Update superfences configuration. if "pymdownx.superfences" not in config["markdown_extensions"]: config["markdown_extensions"].append("pymdownx.superfences") if "pymdownx.superfences" not in config["mdx_configs"]: @@ -864,6 +873,13 @@ def _shim_markdown_exec(config: dict[str, Any]) -> None: superfences["custom_fences"].extend( _get_markdown_exec_superfences(languages) ) + # Save configuration in markdown-exec. + # We must pass the original, mutable references, + # since other shims and our `render` function might modify them. + markdown_exec.markdown_config.save( + config["markdown_extensions"], + config["mdx_configs"], + ) def _shim_mkdocstrings(config: dict[str, Any]) -> None: @@ -920,13 +936,8 @@ def _get_markdown_exec_superfences( languages: list[str] | None = None, ) -> list[dict[str, Any]]: """Get superfences configuration for markdown-exec.""" - try: - import markdown_exec # noqa: PLC0415 # ty:ignore[unresolved-import] - except ImportError as error: - raise ConfigurationError( - "markdown-exec plugin is enabled, but markdown-exec is not " - "installed. Please install markdown-exec or disable the plugin." - ) from error + import markdown_exec # noqa: PLC0415 # ty:ignore[unresolved-import] + return [ { "name": language, diff --git a/python/zensical/extensions/context.py b/python/zensical/extensions/context.py index a265cc8..3835122 100644 --- a/python/zensical/extensions/context.py +++ b/python/zensical/extensions/context.py @@ -94,9 +94,6 @@ class ContextExtension(Extension): def extendMarkdown(self, md: Markdown) -> None: """Register rendering context preprocessor.""" - # We must register the extension to ensure markdown-exec - # is able to forward it to its inner Markdown instances - md.registerExtension(self) preprocessor = ContextPreprocessor(md=md, **self._kwargs) md.preprocessors.register(preprocessor, preprocessor.name, 0) diff --git a/python/zensical/extensions/search.py b/python/zensical/extensions/search.py index 0c007e2..9a5445f 100644 --- a/python/zensical/extensions/search.py +++ b/python/zensical/extensions/search.py @@ -101,11 +101,6 @@ class SearchExtension(Extension): def extendMarkdown(self, md: Markdown) -> None: """Register the PostProcessor with Markdown.""" - # We explicitly do not register the extension, - # because it only needs to run in the top conversion layer - # (and not in inner layers like mkdocstrings' or markdown-exec's). - # Indeed it runs last as a postprocessor, - # so is able to see everything generated by inner layers. config = SearchConfig(**self._kwargs) processor = SearchProcessor(md, config) md.postprocessors.register(processor, processor.name, 0) diff --git a/python/zensical/markdown/render.py b/python/zensical/markdown/render.py index 713d532..ac598cd 100644 --- a/python/zensical/markdown/render.py +++ b/python/zensical/markdown/render.py @@ -86,10 +86,10 @@ def render(content: str, path: str, url: str) -> dict: page = Page(url=url, path=path, meta=meta) set_autorefs_page(page) - # Update configuration to include context extension + # Update configuration to include context extension. # It's important we mutate the global configuration here, - # to allow mkdocstrings to forward the extension - # to its inner Markdown instances + # to allow mkdocstrings and markdown-exec to forward + # the extension to their inner Markdown instances. config = get_config() for extension in config["markdown_extensions"]: if isinstance(extension, ContextExtension): @@ -110,6 +110,15 @@ def render(content: str, path: str, url: str) -> dict: extension_configs=config["mdx_configs"], ) + # Note: mkdocstrings and markdown-exec do not need to propagate + # the links and search extensions to their inner Markdown instances: + # their postprocessors run last and can see inner layer contents. + # More importantly, inner layers *must not* run the links and search + # extensions: the inner links treeprocessor would transform links once, + # and the outer links postprocessor would transform them again. + # The search postprocessor would run twice for generated content, + # incurring a performance cost. + # Register links extension, which is equivalent to MkDocs' path resolution # Markdown extension. This is a bandaid, until we move this to Rust links = LinksExtension(