merman-render 0.8.0

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use crate::config::{config_bool, config_f64, config_font_family_css, config_string};
use crate::theme::PresentationTheme;
use serde_json::Value;
use std::collections::HashMap;

const DEFAULT_ROW_INDENT: f64 = 10.0;
const DEFAULT_PADDING_X: f64 = 5.0;
const DEFAULT_PADDING_Y: f64 = 5.0;
const DEFAULT_LINE_THICKNESS: f64 = 1.0;
const DEFAULT_USE_MAX_WIDTH: bool = true;

pub(crate) struct TreeViewConfigView<'a> {
    effective_config: &'a Value,
    tree_view_config: &'a Value,
}

impl<'a> TreeViewConfigView<'a> {
    pub(crate) fn new(effective_config: &'a Value) -> Self {
        Self {
            effective_config,
            tree_view_config: effective_config.get("treeView").unwrap_or(&Value::Null),
        }
    }

    pub(crate) fn layout_settings(&self) -> TreeViewLayoutSettings {
        let theme = PresentationTheme::new(self.effective_config).tree_view();
        TreeViewLayoutSettings {
            row_indent: self
                .tree_view_f64("rowIndent")
                .unwrap_or(DEFAULT_ROW_INDENT)
                .max(0.0),
            padding_x: self
                .tree_view_f64("paddingX")
                .unwrap_or(DEFAULT_PADDING_X)
                .max(0.0),
            padding_y: self
                .tree_view_f64("paddingY")
                .unwrap_or(DEFAULT_PADDING_Y)
                .max(0.0),
            line_thickness: self
                .tree_view_f64("lineThickness")
                .unwrap_or(DEFAULT_LINE_THICKNESS)
                .max(0.0),
            use_max_width: self
                .tree_view_bool("useMaxWidth")
                .unwrap_or(DEFAULT_USE_MAX_WIDTH),
            font_family: config_font_family_css(self.effective_config),
            label_font_size: theme.label_font_size,
            show_icons: self.tree_view_bool("showIcons").unwrap_or(false),
            default_icon_pack: self.tree_view_string("defaultIconPack").unwrap_or_default(),
            filename_icons: self.tree_view_string_map("filenameIcons"),
            extension_icons: self.tree_view_string_map("extensionIcons"),
        }
    }

    fn tree_view_bool(&self, key: &str) -> Option<bool> {
        config_bool(self.tree_view_config, &[key])
    }

    fn tree_view_f64(&self, key: &str) -> Option<f64> {
        config_f64(self.tree_view_config, &[key])
    }

    fn tree_view_string(&self, key: &str) -> Option<String> {
        config_string(self.tree_view_config, &[key])
    }

    fn tree_view_string_map(&self, key: &str) -> HashMap<String, String> {
        self.tree_view_config
            .get(key)
            .and_then(Value::as_object)
            .map(|values| {
                values
                    .iter()
                    .filter_map(|(key, value)| {
                        value.as_str().map(|value| (key.clone(), value.to_string()))
                    })
                    .collect()
            })
            .unwrap_or_default()
    }
}

#[derive(Debug, Clone)]
pub(crate) struct TreeViewLayoutSettings {
    pub(crate) row_indent: f64,
    pub(crate) padding_x: f64,
    pub(crate) padding_y: f64,
    pub(crate) line_thickness: f64,
    pub(crate) use_max_width: bool,
    pub(crate) font_family: String,
    pub(crate) label_font_size: f64,
    pub(crate) show_icons: bool,
    pub(crate) default_icon_pack: String,
    pub(crate) filename_icons: HashMap<String, String>,
    pub(crate) extension_icons: HashMap<String, String>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn tree_view_layout_settings_preserve_defaults_and_theme_font_size() {
        let cfg = json!({});
        let settings = TreeViewConfigView::new(&cfg).layout_settings();

        assert_eq!(settings.row_indent, DEFAULT_ROW_INDENT);
        assert_eq!(settings.padding_x, DEFAULT_PADDING_X);
        assert_eq!(settings.padding_y, DEFAULT_PADDING_Y);
        assert_eq!(settings.line_thickness, DEFAULT_LINE_THICKNESS);
        assert!(settings.use_max_width);
        assert_eq!(
            settings.font_family,
            "\"trebuchet ms\",verdana,arial,sans-serif"
        );
        assert_eq!(settings.label_font_size, 16.0);
        assert!(!settings.show_icons);
        assert_eq!(settings.default_icon_pack, "");
        assert!(settings.filename_icons.is_empty());
        assert!(settings.extension_icons.is_empty());
    }

    #[test]
    fn tree_view_layout_settings_project_configured_values() {
        let cfg = json!({
            "treeView": {
                "rowIndent": "12",
                "paddingX": 7,
                "paddingY": 8,
                "lineThickness": 2,
                "useMaxWidth": false,
                "showIcons": true,
                "defaultIconPack": "logos",
                "filenameIcons": { "Dockerfile": "docker" },
                "extensionIcons": { ".ts": "typescript" }
            },
            "themeVariables": {
                "treeView": {
                    "labelFontSize": "21px"
                }
            }
        });
        let settings = TreeViewConfigView::new(&cfg).layout_settings();

        assert_eq!(settings.row_indent, 12.0);
        assert_eq!(settings.padding_x, 7.0);
        assert_eq!(settings.padding_y, 8.0);
        assert_eq!(settings.line_thickness, 2.0);
        assert!(!settings.use_max_width);
        assert_eq!(settings.label_font_size, 21.0);
        assert!(settings.show_icons);
        assert_eq!(settings.default_icon_pack, "logos");
        assert_eq!(
            settings
                .filename_icons
                .get("Dockerfile")
                .map(String::as_str),
            Some("docker")
        );
        assert_eq!(
            settings.extension_icons.get(".ts").map(String::as_str),
            Some("typescript")
        );
    }

    #[test]
    fn tree_view_layout_settings_clamp_negative_geometry() {
        let cfg = json!({
            "treeView": {
                "rowIndent": -12,
                "paddingX": -7,
                "paddingY": -8,
                "lineThickness": -2
            }
        });
        let settings = TreeViewConfigView::new(&cfg).layout_settings();

        assert_eq!(settings.row_indent, 0.0);
        assert_eq!(settings.padding_x, 0.0);
        assert_eq!(settings.padding_y, 0.0);
        assert_eq!(settings.line_thickness, 0.0);
    }
}