merman-render 0.7.0-alpha.1

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use super::*;
use merman_core::diagrams::tree_view::TreeViewDiagramRenderModel;

pub(super) fn render_tree_view_diagram_svg(
    layout: &TreeViewDiagramLayout,
    semantic: &serde_json::Value,
    effective_config: &serde_json::Value,
    options: &SvgRenderOptions,
) -> Result<String> {
    let model: TreeViewDiagramRenderModel = crate::json::from_value_ref(semantic)?;
    render_tree_view_diagram_svg_model(layout, &model, effective_config, options)
}

pub(super) fn render_tree_view_diagram_svg_model(
    layout: &TreeViewDiagramLayout,
    model: &TreeViewDiagramRenderModel,
    effective_config: &serde_json::Value,
    options: &SvgRenderOptions,
) -> Result<String> {
    let diagram_id = options.diagram_id.as_deref().unwrap_or("treeView");
    let diagram_id_esc = escape_xml(diagram_id);
    let acc_title = model
        .acc_title
        .as_deref()
        .map(str::trim)
        .filter(|t| !t.is_empty());
    let acc_descr = model
        .acc_descr
        .as_deref()
        .map(str::trim)
        .filter(|d| !d.is_empty());
    let aria_labelledby = acc_title.map(|_| format!("chart-title-{diagram_id_esc}"));
    let aria_describedby = acc_descr.map(|_| format!("chart-desc-{diagram_id_esc}"));
    let min_x = -layout.line_thickness / 2.0;
    let viewbox_width = layout.total_width.max(1.0);
    let viewbox_height = layout.total_height.max(1.0);
    let viewbox_attr = format!(
        "{} 0 {} {}",
        fmt(min_x),
        fmt(viewbox_width),
        fmt(viewbox_height)
    );
    let max_width = fmt_string(viewbox_width);
    let style_attr = format!("max-width: {max_width}px; background-color: white;");
    let fixed_width = fmt_string(viewbox_width);
    let fixed_height = fmt_string(viewbox_height);

    let mut out = String::new();
    if layout.use_max_width {
        root_svg::push_svg_root_open(
            &mut out,
            root_svg::SvgRootAttrs {
                width: root_svg::SvgRootWidth::Percent100,
                style_attr: Some(style_attr.as_str()),
                viewbox_attr: Some(viewbox_attr.as_str()),
                aria_labelledby: aria_labelledby.as_deref(),
                aria_describedby: aria_describedby.as_deref(),
                trailing_newline: false,
                ..root_svg::SvgRootAttrs::new(diagram_id, "treeView")
            },
        );
    } else {
        root_svg::push_svg_root_open(
            &mut out,
            root_svg::SvgRootAttrs {
                width: root_svg::SvgRootWidth::Fixed(fixed_width.as_str()),
                height_attr: Some(fixed_height.as_str()),
                style_attr: Some("background-color: white;"),
                viewbox_attr: Some(viewbox_attr.as_str()),
                aria_labelledby: aria_labelledby.as_deref(),
                aria_describedby: aria_describedby.as_deref(),
                trailing_newline: false,
                ..root_svg::SvgRootAttrs::new(diagram_id, "treeView")
            },
        );
    }

    let css = tree_view_css(effective_config);
    if let Some(title) = acc_title {
        let _ = write!(
            &mut out,
            r#"<title id="chart-title-{}">{}</title>"#,
            diagram_id_esc,
            escape_xml_display(title)
        );
    }
    if let Some(descr) = acc_descr {
        let _ = write!(
            &mut out,
            r#"<desc id="chart-desc-{}">{}</desc>"#,
            diagram_id_esc,
            escape_xml_display(descr)
        );
    }
    let _ = write!(&mut out, "<style>{css}</style>");
    out.push_str("<g/>");
    out.push_str(r#"<g class="tree-view">"#);
    for node in &layout.nodes {
        let _ = write!(
            &mut out,
            r#"<text dominant-baseline="middle" class="treeView-node-label" x="{}" y="{}">{}</text>"#,
            fmt(node.label_x),
            fmt(node.label_y),
            escape_xml(&node.name)
        );
    }
    for line in &layout.lines {
        let _ = write!(
            &mut out,
            r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke-width="{}" class="treeView-node-line"></line>"#,
            fmt(line.x1),
            fmt(line.y1),
            fmt(line.x2),
            fmt(line.y2),
            fmt(line.stroke_width)
        );
    }
    out.push_str("</g></svg>\n");
    Ok(out)
}

fn tree_view_css(effective_config: &serde_json::Value) -> String {
    let label_font_size = crate::config::config_css_number_or_string(
        effective_config,
        &["themeVariables", "treeView", "labelFontSize"],
    )
    .unwrap_or_else(|| "16px".to_string());
    let label_color = config_string(
        effective_config,
        &["themeVariables", "treeView", "labelColor"],
    )
    .unwrap_or_else(|| "black".to_string());
    let line_color = config_string(
        effective_config,
        &["themeVariables", "treeView", "lineColor"],
    )
    .unwrap_or_else(|| "black".to_string());

    format!(
        ".treeView-node-label {{ font-size: {label_font_size}; fill: {label_color}; }} .treeView-node-line {{ stroke: {line_color}; }}"
    )
}