diff --git a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav.rs b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav.rs index 8771d13..52cb5a4 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav.rs @@ -3,6 +3,26 @@ // SPDX-License-Identifier: MIT // All contributions are certified under the DCO +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + //! Native compatibility pipeline for filesystem-backed awesome navigation. use anyhow::{bail, Context, Result}; @@ -26,6 +46,21 @@ mod pattern; mod resolver; mod sort; +// ---------------------------------------------------------------------------- +// Enums +// ---------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Level { + Info, + Warning, + Error, +} + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + /// Native awesome-nav pipeline. #[derive(Clone, Debug)] pub struct AwesomeNav { @@ -40,29 +75,22 @@ pub struct Dependencies<'a> { pub pages: &'a Stream, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Level { - Info, - Warning, - Error, -} - #[derive(Clone, Debug)] -pub struct Diagnostic { - pub level: Level, - pub message: String, +struct Diagnostic { + level: Level, + message: String, } #[derive(Clone, Copy, Debug)] -pub struct Logs { - pub nav_override: Level, - pub root_title: Level, - pub root_hide: Level, - pub no_matches: Level, +struct Logs { + nav_override: Level, + root_title: Level, + root_hide: Level, + no_matches: Level, } #[derive(Clone, Debug)] -pub struct Settings { +struct Settings { enabled: bool, docs: String, filename: String, @@ -89,6 +117,10 @@ struct Pages(Arc>); impl Value for Pages {} +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + impl AwesomeNav { /// Resolves immutable settings for one workflow lifetime. pub fn new(config: &Config, strict: bool) -> Result { @@ -183,6 +215,10 @@ impl Logs { } } +// ---------------------------------------------------------------------------- +// Functions +// ---------------------------------------------------------------------------- + fn level(value: Option<&str>, default: Level) -> Result { match value { None => Ok(default), @@ -219,6 +255,10 @@ fn is_config_file(path: &SourcePath, filename: &str) -> bool { .is_some_and(|prefix| prefix.ends_with('/')) } +// ---------------------------------------------------------------------------- +// Tests +// ---------------------------------------------------------------------------- + #[cfg(test)] mod tests { use super::{is_config_file, level, Level}; diff --git a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/config.rs b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/config.rs index 7a179fd..1bc202c 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/config.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/config.rs @@ -3,6 +3,26 @@ // SPDX-License-Identifier: MIT // All contributions are certified under the DCO +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + //! Strict `.nav.yml` parsing and configuration models. use anyhow::{bail, Context, Result}; diff --git a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/pattern.rs b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/pattern.rs index 1548d62..52df40e 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/pattern.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/pattern.rs @@ -3,6 +3,26 @@ // SPDX-License-Identifier: MIT // All contributions are certified under the DCO +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + //! Replaceable awesome-nav pattern matching boundary. use anyhow::{bail, Result}; diff --git a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/resolver.rs b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/resolver.rs index 1c1416e..29fb37e 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/resolver.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/resolver.rs @@ -3,6 +3,26 @@ // SPDX-License-Identifier: MIT // All contributions are certified under the DCO +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + //! Filesystem-derived awesome-nav resolution. use anyhow::{bail, Context, Result}; diff --git a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/sort.rs b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/sort.rs index 2fdc5fa..dfe9c4f 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/sort.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/awesome_nav/sort.rs @@ -3,6 +3,26 @@ // SPDX-License-Identifier: MIT // All contributions are certified under the DCO +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + //! Awesome-nav sorting compatible with upstream's natsort settings. use std::cmp::Ordering; diff --git a/crates/zensical/src/compat/mkdocs/plugin/literate_nav.rs b/crates/zensical/src/compat/mkdocs/plugin/literate_nav.rs index 7409e83..f64a5a2 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/literate_nav.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/literate_nav.rs @@ -15,7 +15,7 @@ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS diff --git a/crates/zensical/src/compat/mkdocs/plugin/literate_nav/resolver.rs b/crates/zensical/src/compat/mkdocs/plugin/literate_nav/resolver.rs index 5b0e3c4..d4007fa 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/literate_nav/resolver.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/literate_nav/resolver.rs @@ -15,7 +15,7 @@ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS diff --git a/crates/zensical/src/compat/mkdocs/plugin/meta.rs b/crates/zensical/src/compat/mkdocs/plugin/meta.rs index 6d61056..4285c60 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/meta.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/meta.rs @@ -173,7 +173,7 @@ impl Meta { } /// Returns the immutable settings shared with resource classification. - pub(crate) fn settings(&self) -> &Settings { + pub fn settings(&self) -> &Settings { &self.settings } } diff --git a/crates/zensical/src/compat/mkdocs/plugin/minify.rs b/crates/zensical/src/compat/mkdocs/plugin/minify.rs index 71a6268..c6e1191 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/minify.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/minify.rs @@ -89,17 +89,17 @@ impl Minify { } /// Returns the normalized plugin configuration for the asset stage. - pub(super) fn config(&self) -> &MinifyPluginConfig { + fn config(&self) -> &MinifyPluginConfig { &self.config } /// Returns the site output directory owned by this pipeline. - pub(super) fn output(&self) -> &OutputRoot { + fn output(&self) -> &OutputRoot { &self.output } /// Returns the template-visible project configuration. - pub(super) fn project(&self) -> &Arc { + fn project(&self) -> &Arc { &self.project } diff --git a/crates/zensical/src/compat/mkdocs/plugin/minify/asset.rs b/crates/zensical/src/compat/mkdocs/plugin/minify/asset.rs index 5cc7bb8..00f2d9e 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/minify/asset.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/minify/asset.rs @@ -336,7 +336,7 @@ impl Value for Manifest {} /// Transforms selected resources, writes every effective asset, and publishes /// the project view whose configured asset paths name the emitted files. -pub(super) fn setup( +pub fn setup( plugin: &Minify, resources: &Stream, ) -> Signal { let settings = Settings::new(plugin.config()); diff --git a/crates/zensical/src/compat/mkdocs/plugin/mkdocstrings.rs b/crates/zensical/src/compat/mkdocs/plugin/mkdocstrings.rs index 04c7999..91f11dd 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/mkdocstrings.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/mkdocstrings.rs @@ -83,19 +83,16 @@ impl Mkdocstrings { module .call_method1("get_inventory", (cached,))? .extract::>() - }); + })?; - if let Ok(data) = data { - let path = pipeline.output.join( - &"objects.inv" - .parse::() - .expect("static site path"), - ); - let _ = fs::create_dir_all(path.parent().expect("invariant")); - let _ = fs::write(path, &data); - let _ = fs::create_dir_all(&pipeline.cache); - let _ = fs::write(&cache_path, &data); - } + let path = pipeline.output.join( + &"objects.inv".parse::().expect("static site path"), + ); + fs::create_dir_all(path.parent().expect("invariant"))?; + fs::write(path, &data)?; + fs::create_dir_all(&pipeline.cache)?; + fs::write(&cache_path, &data)?; + Ok::<_, anyhow::Error>(()) }); } } diff --git a/crates/zensical/src/compat/mkdocs/resource.rs b/crates/zensical/src/compat/mkdocs/resource.rs index b80b35a..b868610 100644 --- a/crates/zensical/src/compat/mkdocs/resource.rs +++ b/crates/zensical/src/compat/mkdocs/resource.rs @@ -89,12 +89,16 @@ impl Resources { } /// Classifies sources and resolves docs-over-theme precedence. - pub fn setup(&self, deps: Dependencies<'_>) -> Stream { + pub fn setup( + &self, dependencies: Dependencies<'_>, + ) -> Stream { let classifier = self.classifier.clone(); let resources = - deps.sources.filter_map(move |id: &Id, source: &Source| { - classifier.classify(id, source) - }); + dependencies + .sources + .filter_map(move |id: &Id, source: &Source| { + classifier.classify(id, source) + }); // Settle precedence before consumers transform or write a resource. // Removing a docs override therefore reveals its theme fallback in diff --git a/crates/zensical/src/lib.rs b/crates/zensical/src/lib.rs index 45d589a..5da6914 100644 --- a/crates/zensical/src/lib.rs +++ b/crates/zensical/src/lib.rs @@ -239,7 +239,7 @@ fn run(config_file: &PathBuf, mode: Mode) -> PyResult { let run = runner .settle() .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - report_failures(&run)?; + report_failures(&run); // Create channel for reload notifications let (sender, receiver) = unbounded(); @@ -315,7 +315,7 @@ fn run(config_file: &PathBuf, mode: Mode) -> PyResult { let run = runner .settle() .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - report_failures(&run)?; + report_failures(&run); } Err(RecvTimeoutError::Timeout) => {} Err(RecvTimeoutError::Disconnected) => match mode { @@ -345,14 +345,21 @@ fn run(config_file: &PathBuf, mode: Mode) -> PyResult { Ok(false) } -/// Returns the first action failure reported by one settled run. -fn report_failures(run: &zrx::stream::Run) -> PyResult<()> { - for invocation in run.report().invocations() { - if let Some(failure) = invocation.outcomes.failures().first() { - return Err(PyRuntimeError::new_err(format!("{failure:#}"))); - } +/// Prints action failures reported by one settled run. +fn report_failures(run: &zrx::stream::Run) { + for failure in reported_failures(run) { + eprintln!("Error - {failure}"); } - Ok(()) +} + +/// Formats action failures reported by one settled run. +fn reported_failures(run: &zrx::stream::Run) -> Vec { + run.report() + .invocations() + .iter() + .flat_map(|invocation| invocation.outcomes.failures()) + .map(|failure| format!("{failure:#}")) + .collect() } // ---------------------------------------------------------------------------- @@ -410,8 +417,32 @@ fn zensical(m: &Bound<'_, PyModule>) -> PyResult<()> { mod tests { use std::fs; use tempfile::tempdir; + use zrx::id::Id; + use zrx::stream::{Key, Workflow}; - use super::clear_dir; + use super::{clear_dir, reported_failures}; + + #[test] + fn operator_failures_are_formatted_for_cli_reporting() { + let workflow = Workflow::::build(|workflow| { + let input = workflow.input::(); + let output = input.map(|_: &u64| -> anyhow::Result { + anyhow::bail!("broken callback") + }); + workflow.output(&output); + }); + let mut runner = workflow.runner().unwrap(); + let input = runner.input::().unwrap(); + let mut revision = input.begin().unwrap(); + revision + .insert(Key::from_iter(std::iter::empty()), 1) + .unwrap(); + let _input = revision.seal().unwrap(); + let run = runner.settle().unwrap(); + + assert_eq!(reported_failures(&run), ["broken callback"]); + assert_eq!(runner.errors().len(), 1); + } #[test] fn clear_dir_removes_non_hidden_file() { diff --git a/python/tests/unit/extensions/test_links.py b/python/tests/unit/extensions/test_links.py index 6250667..4efffa7 100644 --- a/python/tests/unit/extensions/test_links.py +++ b/python/tests/unit/extensions/test_links.py @@ -15,7 +15,7 @@ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS diff --git a/python/tests/unit/test_config.py b/python/tests/unit/test_config.py index f509847..14dc6ce 100644 --- a/python/tests/unit/test_config.py +++ b/python/tests/unit/test_config.py @@ -171,6 +171,18 @@ class TestPluginShimming: config = self._parse_yaml(tmp_path, plugins={"glightbox": {}}) assert GlightboxExtension.name in config["markdown_extensions"] + @pytest.mark.parametrize( + "entry", ["material/meta", {"material/meta": None}] + ) + def test_material_meta_presence_enables_defaults( + self, tmp_path: Path, entry: object + ) -> None: + config = self._parse_yaml(tmp_path, plugins=[entry]) + assert config["plugins"]["meta"]["config"] == { + "enabled": True, + "meta_file": ".meta.yml", + } + def test_material_meta_plugin_is_normalized(self, tmp_path: Path) -> None: config = self._parse_yaml( tmp_path, @@ -183,6 +195,25 @@ class TestPluginShimming: } assert config["plugins_hash"] == cfg_module._hash(config["plugins"]) + @pytest.mark.parametrize( + "name", + [ + "material/meta", + "redirects", + "minify", + "literate-nav", + "awesome-nav", + ], + ) + def test_native_plugin_configuration_must_be_a_mapping( + self, tmp_path: Path, name: str + ) -> None: + with pytest.raises( + cfg_module.ConfigurationError, + match=rf"{name} configuration must be a mapping", + ): + self._parse_yaml(tmp_path, plugins={name: []}) + def test_redirects_plugin_is_normalized(self, tmp_path: Path) -> None: config = self._parse_yaml( tmp_path, @@ -292,7 +323,6 @@ class TestPluginShimming: @pytest.mark.parametrize( ("plugin", "message"), [ - ([], "configuration must be a mapping"), ({"unknown": True}, "unknown awesome-nav option"), ({"filename": 42}, "filename must be a string"), ({"filename": ""}, "filename must not be empty"), diff --git a/python/zensical/compat/literate_nav.py b/python/zensical/compat/literate_nav.py index aaade21..97e81b6 100644 --- a/python/zensical/compat/literate_nav.py +++ b/python/zensical/compat/literate_nav.py @@ -15,7 +15,7 @@ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS diff --git a/python/zensical/config.py b/python/zensical/config.py index 9afbd2a..bdca679 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -1292,8 +1292,8 @@ def _convert_plugin_markdown_extensions( def _convert_plugins(value: Any, config: dict) -> dict: """Convert plugins configuration to something we can work with.""" - plugins = {} - tags = [] + plugins: dict[str, Any] = {} + tags: list[dict[str, Any]] = [] def add(name: str, data: Any) -> None: """Preserve tags instances while retaining legacy map semantics.""" @@ -1334,35 +1334,23 @@ def _convert_plugins(value: Any, config: dict) -> dict: # Consume Material's public plugin name and normalize it to the internal # identifier extracted into typed Rust configuration. - material_meta = plugins.pop("material/meta", None) - if material_meta is None: - meta = {"enabled": False, "meta_file": ".meta.yml"} - else: - meta = dict(material_meta or {}) - set_default(meta, "enabled", True, bool) - set_default(meta, "meta_file", ".meta.yml", str) + present, meta = _pop_plugin_config(plugins, "material/meta") + set_default(meta, "enabled", present, bool) + set_default(meta, "meta_file", ".meta.yml", str) plugins["meta"] = meta # Normalize redirects into typed native configuration. The enabled flag is # internal; plugin presence retains MkDocs' activation semantics. - if "redirects" not in plugins: - redirects = {"enabled": False, "redirect_maps": {}} - else: - redirects = dict(plugins["redirects"] or {}) - set_default(redirects, "enabled", True, bool) - set_default(redirects, "redirect_maps", {}, dict) + present, redirects = _pop_plugin_config(plugins, "redirects") + set_default(redirects, "enabled", present, bool) + set_default(redirects, "redirect_maps", {}, dict) plugins["redirects"] = redirects # Normalize the complete mkdocs-minify-plugin configuration surface. Asset # settings are retained for the dedicated copy/output stage; the inline # switches are Zensical extensions handled by the final HTML pass. - minify: dict[str, Any] - if "minify" not in plugins: - minify = {"enabled": False} - else: - minify = dict(plugins["minify"] or {}) - set_default(minify, "enabled", True, bool) - + present, minify = _pop_plugin_config(plugins, "minify") + set_default(minify, "enabled", present, bool) set_default(minify, "minify_html", False, bool) set_default(minify, "minify_js", False, bool) set_default(minify, "minify_css", False, bool) @@ -1401,13 +1389,8 @@ def _convert_plugins(value: Any, config: dict) -> dict: # Normalize mkdocs-literate-nav without importing or executing the plugin. # Python retains extension objects and callables for the narrow Markdown # rendering boundary; Rust owns discovery and navigation resolution. - literate_nav: dict[str, Any] - if "literate-nav" not in plugins: - literate_nav = {"enabled": False} - else: - literate_nav_config = plugins.pop("literate-nav") - literate_nav = dict(literate_nav_config or {}) - set_default(literate_nav, "enabled", True, bool) + present, literate_nav = _pop_plugin_config(plugins, "literate-nav") + set_default(literate_nav, "enabled", present, bool) set_default(literate_nav, "nav_file", "SUMMARY.md", str) set_default(literate_nav, "implicit_index", False, bool) set_default(literate_nav, "tab_length", 4, int) @@ -1420,19 +1403,8 @@ def _convert_plugins(value: Any, config: dict) -> dict: # Normalize mkdocs-awesome-nav without importing or executing the plugin. # Rust owns discovery, YAML parsing, matching and navigation resolution. - awesome_nav: dict[str, Any] - if "awesome-nav" not in plugins: - awesome_nav = {"enabled": False} - else: - awesome_nav_config = plugins.pop("awesome-nav") - if awesome_nav_config is not None and not isinstance( - awesome_nav_config, dict - ): - raise ConfigurationError( - "awesome-nav configuration must be a mapping" - ) - awesome_nav = dict(awesome_nav_config or {}) - set_default(awesome_nav, "enabled", True, bool) + present, awesome_nav = _pop_plugin_config(plugins, "awesome-nav") + set_default(awesome_nav, "enabled", present, bool) unknown = set(awesome_nav) - {"enabled", "filename", "logs"} if unknown: option = sorted(unknown)[0] @@ -1516,3 +1488,17 @@ def _convert_plugins(value: Any, config: dict) -> dict: # Return plugins return plugins + + +def _pop_plugin_config( + plugins: dict[str, Any], name: str +) -> tuple[bool, dict[str, Any]]: + """Consume one optional mapping while preserving presence semantics.""" + if name not in plugins: + return False, {} + value = plugins.pop(name) + if value is None: + return True, {} + if not isinstance(value, dict): + raise ConfigurationError(f"{name} configuration must be a mapping") + return True, dict(value)