diff --git a/crates/zensical/src/compat/mkdocs/plugin/tags.rs b/crates/zensical/src/compat/mkdocs/plugin/tags.rs index d486b3c..f2fa6ee 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/tags.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/tags.rs @@ -83,7 +83,7 @@ pub struct Dependencies<'a> { struct Instance { /// Stable configuration-order identity. id: usize, - /// Public MkDocs plugin instance name. + /// Canonical plugin name. name: String, /// Fully normalized compatibility configuration. config: Arc, diff --git a/crates/zensical/src/config/plugins/tags.rs b/crates/zensical/src/config/plugins/tags.rs index d61b76e..a5588cb 100644 --- a/crates/zensical/src/config/plugins/tags.rs +++ b/crates/zensical/src/config/plugins/tags.rs @@ -101,10 +101,10 @@ pub struct TagsPlugin { // ---------------------------------------------------------------------------- -/// One named Material tags plugin instance. +/// One Material tags plugin instance. #[derive(Clone, Debug, Hash, Serialize)] pub struct TagsPluginInstance { - /// Public MkDocs plugin instance name. + /// Canonical plugin name. pub name: String, /// Plugin configuration. pub config: TagsPluginConfig, diff --git a/python/tests/integration/test_tags.py b/python/tests/integration/test_tags.py index 960b061..9ec38c0 100644 --- a/python/tests/integration/test_tags.py +++ b/python/tests/integration/test_tags.py @@ -127,6 +127,39 @@ def test_builds_listings_references_toc_and_search_without_export( assert not (tmp_path / "site" / "tags.json").exists() +def test_inherits_tags_from_meta_file(tmp_path: Path) -> None: + """Tags supplied by Material meta participate in page tag mappings.""" + _write_project(tmp_path) + config = tmp_path / "zensical.toml" + config.write_text( + """\ +[project] +site_name = "Tags" + +[project.theme] +custom_dir = "overrides" + +[project.plugins.search] + +[project.plugins.tags] +tags_hierarchy = true + +[project.plugins.meta] +""", + encoding="utf-8", + ) + guide = tmp_path / "docs" / "guide" + (guide / ".meta.yml").write_text("tags: [Inherited]\n", encoding="utf-8") + (guide / "rust.md").write_text( + "---\ntitle: Rust page\n---\n# Rust\n", encoding="utf-8" + ) + + zensical.build(str(config), _BUILD_OPTIONS) + + output = (tmp_path / "site" / "guide" / "rust" / "index.html").read_text() + assert ' None: """Inline filters apply while escaped examples remain ordinary content.""" config = _write_project(tmp_path) diff --git a/python/tests/unit/test_config.py b/python/tests/unit/test_config.py index 14dc6ce..9bc3fe4 100644 --- a/python/tests/unit/test_config.py +++ b/python/tests/unit/test_config.py @@ -172,9 +172,10 @@ class TestPluginShimming: assert GlightboxExtension.name in config["markdown_extensions"] @pytest.mark.parametrize( - "entry", ["material/meta", {"material/meta": None}] + "entry", + ["meta", {"meta": None}, "material/meta", {"material/meta": None}], ) - def test_material_meta_presence_enables_defaults( + def test_meta_presence_enables_defaults( self, tmp_path: Path, entry: object ) -> None: config = self._parse_yaml(tmp_path, plugins=[entry]) @@ -195,22 +196,28 @@ class TestPluginShimming: } assert config["plugins_hash"] == cfg_module._hash(config["plugins"]) + def test_material_offline_alias_is_normalized(self, tmp_path: Path) -> None: + config = self._parse_yaml(tmp_path, plugins=["material/offline"]) + assert "material/offline" not in config["plugins"] + assert config["plugins"]["offline"]["config"]["enabled"] is True + assert config["use_directory_urls"] is False + @pytest.mark.parametrize( - "name", + ("name", "canonical"), [ - "material/meta", - "redirects", - "minify", - "literate-nav", - "awesome-nav", + ("material/meta", "meta"), + ("redirects", "redirects"), + ("minify", "minify"), + ("literate-nav", "literate-nav"), + ("awesome-nav", "awesome-nav"), ], ) def test_native_plugin_configuration_must_be_a_mapping( - self, tmp_path: Path, name: str + self, tmp_path: Path, name: str, canonical: str ) -> None: with pytest.raises( cfg_module.ConfigurationError, - match=rf"{name} configuration must be a mapping", + match=rf"{canonical} configuration must be a mapping", ): self._parse_yaml(tmp_path, plugins={name: []}) @@ -397,7 +404,7 @@ class TestPluginShimming: plugins=[ {"tags": {"listings_directive": "$tags"}}, { - "material/tags/private": { + "material/tags": { "filters": {"include": ["private/**"]}, "tags_name_property": "labels", } @@ -407,7 +414,7 @@ class TestPluginShimming: instances = config["plugins"]["tags"]["config"] assert [instance["name"] for instance in instances] == [ "tags", - "material/tags/private", + "tags", ] assert instances[0]["config"]["listings_directive"] == "$tags" assert instances[1]["config"]["filters"] == { diff --git a/python/zensical/config.py b/python/zensical/config.py index bdca679..a05e279 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -1296,10 +1296,9 @@ def _convert_plugins(value: Any, config: dict) -> dict: tags: list[dict[str, Any]] = [] def add(name: str, data: Any) -> None: - """Preserve tags instances while retaining legacy map semantics.""" - if name in ("tags", "material/tags") or name.startswith( - ("tags/", "material/tags/") - ): + """Canonicalize Material aliases while preserving tag instances.""" + name = name.removeprefix("material/") + if name == "tags": tags.append({"name": name, "config": dict(data or {})}) else: plugins[name] = data @@ -1332,9 +1331,9 @@ def _convert_plugins(value: Any, config: dict) -> dict: search, "separator", '[\\s\\-_,:!=\\[\\]()\\\\"`/]+|\\.(?!\\d)', str ) - # Consume Material's public plugin name and normalize it to the internal - # identifier extracted into typed Rust configuration. - present, meta = _pop_plugin_config(plugins, "material/meta") + # Normalize metadata into the typed Rust configuration. The Material + # namespace is removed at admission, so both names share this code path. + present, meta = _pop_plugin_config(plugins, "meta") set_default(meta, "enabled", present, bool) set_default(meta, "meta_file", ".meta.yml", str) plugins["meta"] = meta