From e7876aba2a5171b0e5ac977ad4bc2201d8053271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Sep 2026 23:33:22 +0200 Subject: [PATCH] feature: support mike's version selector setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- crates/zensical/src/config/plugins.rs | 22 +++++++++ python/tests/integration/test_config.py | 60 +++++++++++++++++++++++++ python/tests/unit/test_config.py | 16 +++++++ python/tests/unit/test_plugin_config.py | 1 + python/zensical/config.py | 7 ++- 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/zensical/src/config/plugins.rs b/crates/zensical/src/config/plugins.rs index 6de8475..1a89766 100644 --- a/crates/zensical/src/config/plugins.rs +++ b/crates/zensical/src/config/plugins.rs @@ -60,6 +60,10 @@ pub struct Plugins { #[pyo3(default)] #[serde(skip_serializing_if = "Option::is_none")] pub autorefs: Option, + /// Mike plugin, when explicitly configured or used for this build. + #[pyo3(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub mike: Option, /// Search plugin. pub search: SearchPlugin, /// Material meta plugin. @@ -122,6 +126,24 @@ impl Default for AutorefsTitleSetting { // ---------------------------------------------------------------------------- +/// Mike plugin. +#[derive(Clone, Debug, Hash, FromPyObject, Serialize)] +#[pyo3(from_item_all)] +pub struct MikePlugin { + /// Plugin configuration. + pub config: MikePluginConfig, +} + +/// Mike plugin configuration used by the theme. +#[derive(Clone, Debug, Hash, FromPyObject, Serialize)] +#[pyo3(from_item_all)] +pub struct MikePluginConfig { + /// Whether to show the version selector. + pub version_selector: bool, +} + +// ---------------------------------------------------------------------------- + /// Awesome navigation plugin. #[derive(Clone, Debug, Hash, FromPyObject, Serialize)] #[pyo3(from_item_all)] diff --git a/python/tests/integration/test_config.py b/python/tests/integration/test_config.py index 93790bf..ccf0df5 100644 --- a/python/tests/integration/test_config.py +++ b/python/tests/integration/test_config.py @@ -30,6 +30,8 @@ from __future__ import annotations +import json +import re from typing import TYPE_CHECKING, Any import pytest @@ -156,6 +158,64 @@ def test_build_with_config_in_watch( assert (tmp_path / "site" / "index.html").is_file() +@pytest.mark.parametrize("config_format", ["yaml", "toml"]) +@pytest.mark.parametrize( + ("enabled", "version_selector", "expected"), + [ + (None, None, True), + (None, True, True), + (None, False, False), + (True, False, False), + (False, False, True), + ], +) +def test_mike_version_selector_controls_theme_configuration( + tmp_path: Path, + config_format: str, + enabled: bool | None, + version_selector: bool | None, + expected: bool, +) -> None: + # Only an active mike plugin can disable the theme's version selector. + value = "" if version_selector is None else str(version_selector).lower() + if config_format == "yaml": + mike = ( + " mike: {}" + if not value + else f" mike:\n version_selector: {value}" + ) + if enabled is not None: + mike += f"\n enabled: {str(enabled).lower()}" + config = _make_yml_project( + tmp_path, + yml_extra=( + f"extra:\n version:\n provider: mike\nplugins:\n{mike}" + ), + ) + else: + option = "" if not value else f"\nversion_selector = {value}" + if enabled is not None: + option += f"\nenabled = {str(enabled).lower()}" + config = _make_toml_project( + tmp_path, + toml_extra=( + '[project.extra.version]\nprovider = "mike"\n' + f"[project.plugins.mike]{option}" + ), + ) + + _build(config) + + html = (tmp_path / "site" / "index.html").read_text(encoding="utf-8") + match = re.search( + r'', html + ) + assert match is not None + settings = json.loads(match.group(1)) + expected_version = {"provider": "mike"} if expected else None + assert settings["version"] == expected_version + + def test_navigation_title_precedes_metadata_and_heading(tmp_path: Path) -> None: """Configured titles have the same highest precedence as in MkDocs.""" config = _make_yml_project( diff --git a/python/tests/unit/test_config.py b/python/tests/unit/test_config.py index 6961598..385199d 100644 --- a/python/tests/unit/test_config.py +++ b/python/tests/unit/test_config.py @@ -588,8 +588,24 @@ class TestPluginShimming: "redirect_template": None, "deploy_prefix": "", "canonical_version": None, + "version_selector": True, } + def test_disabled_mike_does_not_adjust_versioned_site_url( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + # A disabled plugin must stay inactive even during a mike deployment. + monkeypatch.setenv("MIKE_DOCS_VERSION", "0.3") + + config = self._parse_yaml( + tmp_path, + site_url="https://example.com/docs/", + plugins={"mike": {"enabled": False, "version_selector": False}}, + ) + + assert config["site_url"] == "https://example.com/docs/" + assert "mike" not in config["plugins"] + def test_glightbox_adds_extension_and_forwards_config( self, tmp_path: Path ) -> None: diff --git a/python/tests/unit/test_plugin_config.py b/python/tests/unit/test_plugin_config.py index 5fccd2b..48184fd 100644 --- a/python/tests/unit/test_plugin_config.py +++ b/python/tests/unit/test_plugin_config.py @@ -181,6 +181,7 @@ def test_normalizes_mike_defaults() -> None: "redirect_template": None, "deploy_prefix": "", "canonical_version": None, + "version_selector": True, } diff --git a/python/zensical/config.py b/python/zensical/config.py index 7bc2bd8..7b6ed51 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -1957,6 +1957,7 @@ def _convert_plugins(value: Any, config: dict) -> dict: *nullable_strings, }, ) + set_default(mike, "version_selector", True) _validate_boolean_options("mike", mike, ("enabled", "version_selector")) for name, default in string_defaults.items(): set_default(mike, name, default) @@ -1967,7 +1968,9 @@ def _convert_plugins(value: Any, config: dict) -> dict: raise ConfigurationError( f"mike {name} must be a string or null" ) - plugins["mike"] = mike + # Disabled plugins must not affect the theme or versioned site URL. + if mike.get("enabled", True): + plugins["mike"] = mike # Validate settings for plugins enabled through Markdown extensions. if "autorefs" in plugins: @@ -2275,7 +2278,7 @@ def _apply_mike_plugin( version: str | None, ) -> None: """Apply project-wide effects of a mike versioned build.""" - if not version or not config.get("site_url"): + if not version or not config.get("site_url") or "mike" not in plugins: return mike = plugins["mike"]["config"]