Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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).

<!-- insertion marker -->
## [Unreleased](https://github.com/pawamoy/markdown-exec/releases/tag/unreleased)

<small>[Compare with 1.10.0](https://github.com/pawamoy/markdown-exec/compare/1.10.0...HEAD)</small>

### Features

- Allow configuring auto-exec languages in mkdocs.yml ([#76](https://github.com/pawamoy/markdown-exec/issues/76))

## [1.10.0](https://github.com/pawamoy/markdown-exec/releases/tag/1.10.0) - 2024-12-06

<small>[Compare with 1.9.3](https://github.com/pawamoy/markdown-exec/compare/1.9.3...1.10.0)</small>
Expand Down
23 changes: 23 additions & 0 deletions docs/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,29 @@ to the MkDocs configuration file directory, instead of relative to the current
working directory. This will make it possible to use the `-f` option of MkDocs,
to build the documentation from a different directory than the repository root.

### Plugin Configuration Options

The plugin supports the following configuration options:

```yaml
# mkdocs.yml
plugins:
- search
- markdown-exec:
ansi: auto # Whether the ansi extra is required (auto, off, required, true, false)
languages: # Which languages to enable the extension for
- python
- bash
- sh
# ... etc
auto_exec: # Languages for which to automatically execute code blocks
- python
- bash
# Or as a comma-separated string: "python,bash"
```

The `auto_exec` option allows you to configure automatic execution of code blocks for specific languages directly in your `mkdocs.yml` file, instead of using the `MARKDOWN_EXEC_AUTO` environment variable. This makes it easier for new contributors to build your documentation without having to set environment variables.

Example:

```python exec="1" source="material-block"
Expand Down
56 changes: 46 additions & 10 deletions src/markdown_exec/mkdocs_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def __init__(self, prefix: str, logger: logging.Logger) -> None:
super().__init__(logger, {})
self.prefix = prefix

def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
def process(
self, msg: str, kwargs: MutableMapping[str, Any]
) -> tuple[str, MutableMapping[str, Any]]:
return f"{self.prefix}: {msg}", kwargs


Expand All @@ -52,18 +54,29 @@ def _get_logger(name: str) -> _LoggerAdapter:
class MarkdownExecPluginConfig(Config):
"""Configuration of the plugin (for `mkdocs.yml`)."""

ansi = config_options.Choice(("auto", "off", "required", True, False), default="auto")
ansi = config_options.Choice(
("auto", "off", "required", True, False), default="auto"
)
"""Whether the `ansi` extra is required when installing the package."""
languages = config_options.ListOfItems(
config_options.Choice(formatters.keys()),
default=list(formatters.keys()),
)
"""Which languages to enabled the extension for."""
auto_exec = config_options.Type(
(list, str),
default=None,
)
"""Languages for which to automatically execute code blocks."""


class MarkdownExecPlugin(BasePlugin[MarkdownExecPluginConfig]):
"""MkDocs plugin to easily enable custom fences for code blocks execution."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original_env_vars = {}

def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
"""Configure the plugin.

Expand All @@ -88,8 +101,22 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
"that it is installed with the 'ansi' extra. "
"Install it with 'pip install markdown-exec[ansi]'.",
)
self.mkdocs_config_dir = os.getenv("MKDOCS_CONFIG_DIR")

# Save original environment variables
self.original_env_vars = {
"MKDOCS_CONFIG_DIR": os.getenv("MKDOCS_CONFIG_DIR"),
"MARKDOWN_EXEC_AUTO": os.getenv("MARKDOWN_EXEC_AUTO"),
}

# Set MKDOCS_CONFIG_DIR
os.environ["MKDOCS_CONFIG_DIR"] = os.path.dirname(config["config_file_path"])

# Handle auto_exec configuration
if self.config.auto_exec is not None:
if isinstance(self.config.auto_exec, list):
os.environ["MARKDOWN_EXEC_AUTO"] = ",".join(self.config.auto_exec)
else:
os.environ["MARKDOWN_EXEC_AUTO"] = str(self.config.auto_exec)
self.languages = self.config.languages
mdx_configs = config.setdefault("mdx_configs", {})
superfences = mdx_configs.setdefault("pymdownx.superfences", {})
Expand All @@ -113,7 +140,9 @@ def on_env( # noqa: D102
config: MkDocsConfig,
files: Files, # noqa: ARG002
) -> Environment | None:
if self.config.ansi in ("required", True) or (self.config.ansi == "auto" and ansi_ok):
if self.config.ansi in ("required", True) or (
self.config.ansi == "auto" and ansi_ok
):
self._add_css(config, "ansi.css")
if "pyodide" in self.languages:
self._add_css(config, "pyodide.css")
Expand All @@ -123,15 +152,22 @@ def on_env( # noqa: D102
def on_post_build(self, *, config: MkDocsConfig) -> None: # noqa: ARG002,D102
MarkdownConverter.counter = 0
markdown_config.reset()
if self.mkdocs_config_dir is None:
os.environ.pop("MKDOCS_CONFIG_DIR", None)
else:
os.environ["MKDOCS_CONFIG_DIR"] = self.mkdocs_config_dir

def _add_asset(self, config: MkDocsConfig, asset_file: str, asset_type: str) -> None:
# Restore original environment variables
for var, value in self.original_env_vars.items():
if value is None:
os.environ.pop(var, None)
else:
os.environ[var] = value

def _add_asset(
self, config: MkDocsConfig, asset_file: str, asset_type: str
) -> None:
asset_filename = f"assets/_markdown_exec_{asset_file}"
asset_content = Path(__file__).parent.joinpath(asset_file).read_text()
write_file(asset_content.encode("utf-8"), os.path.join(config.site_dir, asset_filename))
write_file(
asset_content.encode("utf-8"), os.path.join(config.site_dir, asset_filename)
)
config[f"extra_{asset_type}"].insert(0, asset_filename)

def _add_css(self, config: MkDocsConfig, css_file: str) -> None:
Expand Down
136 changes: 136 additions & 0 deletions tests/test_mkdocs_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Tests for the MkDocs plugin."""

import os
from unittest.mock import MagicMock, patch

import pytest
from mkdocs.config.defaults import MkDocsConfig

from markdown_exec.mkdocs_plugin import MarkdownExecPlugin, MarkdownExecPluginConfig


@pytest.fixture
def plugin():
"""Return a plugin instance."""
return MarkdownExecPlugin()


@pytest.fixture
def mkdocs_config():
"""Return a MkDocs config."""
config = MkDocsConfig()
config["markdown_extensions"] = ["pymdownx.superfences"]
config["config_file_path"] = "/path/to/mkdocs.yml"
return config


def test_plugin_init():
"""Test plugin initialization."""
plugin = MarkdownExecPlugin()
assert plugin.original_env_vars == {}


def test_auto_exec_list(plugin, mkdocs_config):
"""Test auto_exec with a list value."""
# Set up plugin config
plugin.config = MarkdownExecPluginConfig(auto_exec=["python", "bash"])

# Save original environment
original_auto_exec = os.environ.get("MARKDOWN_EXEC_AUTO")

try:
# Run on_config
with patch("markdown_exec.mkdocs_plugin.ansi_ok", True):
plugin.on_config(mkdocs_config)

# Check that environment variable was set correctly
assert os.environ["MARKDOWN_EXEC_AUTO"] == "python,bash"

# Run on_post_build to restore environment
plugin.on_post_build(config=mkdocs_config)

# Check that environment variable was restored
if original_auto_exec is None:
assert "MARKDOWN_EXEC_AUTO" not in os.environ
else:
assert os.environ["MARKDOWN_EXEC_AUTO"] == original_auto_exec

finally:
# Clean up
if original_auto_exec is None:
os.environ.pop("MARKDOWN_EXEC_AUTO", None)
else:
os.environ["MARKDOWN_EXEC_AUTO"] = original_auto_exec


def test_auto_exec_string(plugin, mkdocs_config):
"""Test auto_exec with a string value."""
# Set up plugin config
plugin.config = MarkdownExecPluginConfig(auto_exec="python,bash")

# Save original environment
original_auto_exec = os.environ.get("MARKDOWN_EXEC_AUTO")

try:
# Run on_config
with patch("markdown_exec.mkdocs_plugin.ansi_ok", True):
plugin.on_config(mkdocs_config)

# Check that environment variable was set correctly
assert os.environ["MARKDOWN_EXEC_AUTO"] == "python,bash"

# Run on_post_build to restore environment
plugin.on_post_build(config=mkdocs_config)

# Check that environment variable was restored
if original_auto_exec is None:
assert "MARKDOWN_EXEC_AUTO" not in os.environ
else:
assert os.environ["MARKDOWN_EXEC_AUTO"] == original_auto_exec

finally:
# Clean up
if original_auto_exec is None:
os.environ.pop("MARKDOWN_EXEC_AUTO", None)
else:
os.environ["MARKDOWN_EXEC_AUTO"] = original_auto_exec


def test_auto_exec_none(plugin, mkdocs_config):
"""Test auto_exec with None value (default)."""
# Set up plugin config
plugin.config = MarkdownExecPluginConfig(auto_exec=None)

# Save original environment
original_auto_exec = os.environ.get("MARKDOWN_EXEC_AUTO")

try:
# Set a test value to ensure it's not changed
if original_auto_exec is None:
os.environ["MARKDOWN_EXEC_AUTO"] = "test"
test_value = "test"
else:
test_value = original_auto_exec

# Run on_config
with patch("markdown_exec.mkdocs_plugin.ansi_ok", True):
plugin.on_config(mkdocs_config)

# Check that environment variable was not changed
assert os.environ["MARKDOWN_EXEC_AUTO"] == test_value

# Run on_post_build to restore environment
plugin.on_post_build(config=mkdocs_config)

# Check that environment variable was restored
if original_auto_exec is None:
assert "MARKDOWN_EXEC_AUTO" not in os.environ
else:
assert os.environ["MARKDOWN_EXEC_AUTO"] == original_auto_exec

finally:
# Clean up
if original_auto_exec is None:
os.environ.pop("MARKDOWN_EXEC_AUTO", None)
else:
os.environ["MARKDOWN_EXEC_AUTO"] = original_auto_exec