diff --git a/python/tests/unit/test_config.py b/python/tests/unit/test_config.py index 4d19038..c905df9 100644 --- a/python/tests/unit/test_config.py +++ b/python/tests/unit/test_config.py @@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Any import pytest import yaml +from markdown import Markdown import zensical.config as cfg_module from zensical.config import ( @@ -194,6 +195,42 @@ def test_watch_excludes_config_file(tmp_path: Path, watch_path: str) -> None: # --------------------------------------------------------------------------- +@pytest.mark.parametrize("value", [None, [], {}, ["abbr"], {"abbr": {}}]) +@pytest.mark.parametrize( + "source", + ["```\n\n```", "- Example\n\n ```\n \n ```"], +) +def test_superfences_is_always_enabled(value: Any, source: str) -> None: + # Fenced code is supported with empty or unrelated extension settings. + extensions, configs = cfg_module._convert_markdown_extensions(value) + + markdown = Markdown(extensions=extensions, extension_configs=configs) + rendered = markdown.convert(source) + + # Both top-level and nested fences are rendered as escaped code blocks. + assert extensions.count("pymdownx.superfences") == 1 + assert configs["pymdownx.superfences"] == {} + assert "
" in rendered
+    assert "<tag>" in rendered
+
+
+@pytest.mark.parametrize(
+    "value",
+    [
+        [{"pymdownx.superfences": {"preserve_tabs": True}}],
+        {"pymdownx.superfences": {"preserve_tabs": True}},
+        {"pymdownx": {"superfences": {"preserve_tabs": True}}},
+    ],
+)
+def test_explicit_superfences_configuration_is_preserved(value: Any) -> None:
+    # SuperFences options can be supplied in YAML or either supported TOML form.
+    extensions, configs = cfg_module._convert_markdown_extensions(value)
+
+    # The extension is added once and its explicit options are retained.
+    assert extensions.count("pymdownx.superfences") == 1
+    assert configs["pymdownx.superfences"] == {"preserve_tabs": True}
+
+
 def test_markdown_extensions_are_normalized_without_mutating_input() -> None:
     value = {
         "abbr": None,
@@ -209,6 +246,7 @@ def test_markdown_extensions_are_normalized_without_mutating_input() -> None:
     assert extensions == [
         "toc",
         "tables",
+        "pymdownx.superfences",
         "abbr",
         "pymdownx.highlight",
         "pymdownx.blocks.tab",
diff --git a/python/zensical/config.py b/python/zensical/config.py
index b2be8e4..4170178 100644
--- a/python/zensical/config.py
+++ b/python/zensical/config.py
@@ -810,10 +810,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
     # Set validation
     config["validation"] = validation
 
-    # MkDocs will also set fenced_code, which is incompatible with SuperFences,
-    # the extension that Material for MkDocs generally recommends. Note that we
-    # decided to set defaults that make it easy to get started with sensible
-    # Markdown support, but users can override this as needed.
+    # Markdown extensions and their configuration are normalized.
     markdown_extensions, mdx_configs = _convert_markdown_extensions(
         config.get("markdown_extensions", DEFAULT_MARKDOWN_EXTENSIONS)
     )
@@ -1563,8 +1560,12 @@ def _convert_markdown_extensions(
     value: Any,
 ) -> tuple[list[str], dict[str, dict[str, Any]]]:
     """Convert Markdown extensions to what Python Markdown expects."""
-    markdown_extensions = ["toc", "tables"]
-    mdx_configs: dict[str, dict[str, Any]] = {"toc": {}, "tables": {}}
+    # As in MkDocs, fenced code is always supported, but SuperFences is used
+    # instead of fenced_code for compatibility with nested blocks.
+    markdown_extensions = ["toc", "tables", "pymdownx.superfences"]
+    mdx_configs: dict[str, dict[str, Any]] = {
+        ext: {} for ext in markdown_extensions
+    }
 
     if value is None:
         return markdown_extensions, mdx_configs