merman-render 0.8.0-alpha.7

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use std::fmt::Write as _;

use super::super::SvgDiagramId;
use super::super::root_svg;
use super::super::util::escape_xml_into;

pub(super) struct FlowchartSvgDocumentRequest<'a> {
    pub family_kind: crate::family::RenderFamilyKind,
    pub diagram_id: SvgDiagramId<'a>,
    pub diagram_type: &'a str,
    pub model: &'a crate::flowchart::FlowchartModel,
    pub use_max_width: bool,
    pub diagram_padding: f64,
    pub bbox_min_x: f64,
    pub bbox_min_y: f64,
    pub bbox_max_x: f64,
    pub bbox_max_y: f64,
}

pub(super) struct FlowchartSvgDocument<'a> {
    diagram_id: SvgDiagramId<'a>,
    diagram_type: &'a str,
    use_max_width: bool,
    root_viewport: root_svg::RootViewportContext<'a>,
    root_spec: root_svg::RootViewportSpec,
    acc_title: Option<&'a str>,
    acc_descr: Option<&'a str>,
    aria_labelledby: Option<String>,
    aria_describedby: Option<String>,
}

pub(super) fn prepare_flowchart_svg_document(
    request: FlowchartSvgDocumentRequest<'_>,
) -> FlowchartSvgDocument<'_> {
    let root_bounds = flowchart_root_bounds(
        request.bbox_min_x,
        request.bbox_min_y,
        request.bbox_max_x,
        request.bbox_max_y,
        request.diagram_padding,
    );
    let root_spec = root_svg::RootViewportSpec::mermaid(root_bounds, request.use_max_width)
        .with_max_width(root_svg::RootMaxWidth::CssSixSignificant(root_bounds.width));
    let root_viewport = root_svg::RootViewportContext::new(request.family_kind, request.diagram_id);

    let acc_title = request
        .model
        .acc_title
        .as_deref()
        .map(|s| s.trim())
        .filter(|s| !s.is_empty());
    let acc_descr = request
        .model
        .acc_descr
        .as_deref()
        .map(|s| s.trim_end_matches('\n'))
        .filter(|s| !s.trim().is_empty());
    let aria_labelledby = acc_title.map(|_| format!("chart-title-{}", request.diagram_id));
    let aria_describedby = acc_descr.map(|_| format!("chart-desc-{}", request.diagram_id));

    FlowchartSvgDocument {
        diagram_id: request.diagram_id,
        diagram_type: request.diagram_type,
        use_max_width: request.use_max_width,
        root_viewport,
        root_spec,
        acc_title,
        acc_descr,
        aria_labelledby,
        aria_describedby,
    }
}

fn flowchart_root_bounds(
    min_x: f64,
    min_y: f64,
    max_x: f64,
    max_y: f64,
    diagram_padding: f64,
) -> root_svg::DiagramBounds {
    root_svg::DiagramBounds::from_extents(min_x, min_y, max_x, max_y, diagram_padding)
}

impl FlowchartSvgDocument<'_> {
    pub(super) fn push_root_open(&self, out: &mut String) -> crate::Result<root_svg::RootDocument> {
        let mut root_chrome = root_svg::RootChrome::new(self.diagram_id, self.diagram_type);
        root_chrome.class = Some(if self.diagram_type == "agentflow" {
            "agentflow"
        } else {
            "flowchart"
        });
        root_chrome.aria_labelledby = self.aria_labelledby.as_deref();
        root_chrome.aria_describedby = self.aria_describedby.as_deref();
        root_chrome.dom.trailing_newline = false;
        if !self.use_max_width {
            root_chrome.dom.style_viewbox_order =
                root_svg::SvgRootStyleViewBoxOrder::ViewBoxThenStyle;
            root_chrome.dom.fixed_height_placement =
                root_svg::SvgRootFixedHeightPlacement::AfterClass;
            root_chrome.dom.fixed_style_placement =
                root_svg::RootStylePlacement::AfterRoleDescription;
        }
        self.root_viewport
            .write_open(out, self.root_spec, root_chrome)
    }

    pub(super) fn push_accessibility_metadata(&self, out: &mut String) {
        if let Some(title) = self.acc_title {
            let _ = write!(out, r#"<title id="chart-title-{}">"#, self.diagram_id);
            escape_xml_into(out, title);
            out.push_str("</title>");
        }
        if let Some(descr) = self.acc_descr {
            let _ = write!(out, r#"<desc id="chart-desc-{}">"#, self.diagram_id);
            escape_xml_into(out, descr);
            out.push_str("</desc>");
        }
    }
}

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

    #[test]
    fn diagram_padding_is_applied_without_a_family_guard() {
        let zero = flowchart_root_bounds(10.0, 20.0, 110.0, 220.0, 0.0);
        assert_eq!(zero.min_x, 10.0);
        assert_eq!(zero.min_y, 20.0);
        assert_eq!(zero.width, 100.0);
        assert_eq!(zero.height, 200.0);

        let fractional = flowchart_root_bounds(10.0, 20.0, 110.0, 220.0, 0.25);
        assert_eq!(fractional.min_x, 9.75);
        assert_eq!(fractional.min_y, 19.75);
        assert_eq!(fractional.width, 100.5);
        assert_eq!(fractional.height, 200.5);

        let padded = flowchart_root_bounds(10.0, 20.0, 110.0, 220.0, 8.0);
        assert_eq!(padded.min_x, 2.0);
        assert_eq!(padded.min_y, 12.0);
        assert_eq!(padded.width, 116.0);
        assert_eq!(padded.height, 216.0);
    }
}