merman-render 0.6.2

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
//! Flowchart render configuration preparation.

use crate::text::{TextStyle, WrapMode};

use super::super::{config_f64, config_string, normalize_css_font_family, theme_color};

pub(in crate::svg::parity::flowchart) struct FlowchartRenderConfig {
    pub font_family: String,
    pub font_size: f64,
    pub wrapping_width: f64,
    pub node_html_labels: bool,
    pub edge_html_labels: bool,
    pub node_wrap_mode: WrapMode,
    pub edge_wrap_mode: WrapMode,
    pub diagram_padding: f64,
    pub use_max_width: bool,
    pub title_top_margin: f64,
    pub node_padding: f64,
    pub text_style: TextStyle,
    pub html_label_text_style: TextStyle,
    pub default_edge_interpolate: String,
    pub default_edge_style: Vec<String>,
    pub node_border_color: String,
    pub node_fill_color: String,
}

pub(in crate::svg::parity::flowchart) fn prepare_flowchart_render_config(
    model: &crate::flowchart::FlowchartV2Model,
    effective_config_value: &serde_json::Value,
) -> FlowchartRenderConfig {
    let default_theme_font_family = "\"trebuchet ms\",verdana,arial,sans-serif".to_string();
    let theme_font_family =
        config_string(effective_config_value, &["themeVariables", "fontFamily"])
            .map(|s| normalize_css_font_family(&s));
    let top_font_family = config_string(effective_config_value, &["fontFamily"])
        .map(|s| normalize_css_font_family(&s));
    let font_family = match (top_font_family, theme_font_family) {
        (Some(top), Some(theme)) if theme == default_theme_font_family => top,
        (_, Some(theme)) => theme,
        (Some(top), None) => top,
        (None, None) => default_theme_font_family,
    };
    let font_size = effective_config_value
        .get("themeVariables")
        .and_then(|tv| tv.get("fontSize"))
        .and_then(parse_font_size_px)
        .unwrap_or(16.0)
        .max(1.0);

    let wrapping_width = config_f64(effective_config_value, &["flowchart", "wrappingWidth"])
        .unwrap_or(200.0)
        .max(1.0);
    let node_html_labels =
        crate::flowchart::flowchart_effective_node_html_labels(effective_config_value);
    let flowchart_html_labels =
        crate::flowchart::flowchart_effective_html_labels(effective_config_value);
    let edge_html_labels = flowchart_html_labels;
    let node_wrap_mode = if node_html_labels {
        WrapMode::HtmlLike
    } else {
        WrapMode::SvgLike
    };
    let edge_wrap_mode = if edge_html_labels {
        WrapMode::HtmlLike
    } else {
        WrapMode::SvgLike
    };
    let diagram_padding = config_f64(effective_config_value, &["flowchart", "diagramPadding"])
        .unwrap_or(8.0)
        .max(0.0);
    let use_max_width = effective_config_value
        .get("flowchart")
        .and_then(|v| v.get("useMaxWidth"))
        .and_then(serde_json::Value::as_bool)
        .unwrap_or(true);
    let title_top_margin = config_f64(effective_config_value, &["flowchart", "titleTopMargin"])
        .unwrap_or(25.0)
        .max(0.0);
    let node_padding = config_f64(effective_config_value, &["flowchart", "padding"])
        .unwrap_or(15.0)
        .max(0.0);

    let text_style = TextStyle {
        font_family: Some(font_family.clone()),
        font_size,
        font_weight: None,
    };
    let html_label_text_style = crate::flowchart::flowchart_html_label_measurement_base_style(
        &text_style,
        effective_config_value,
    );

    let cfg_curve = config_string(effective_config_value, &["flowchart", "curve"]);
    let default_edge_interpolate = model
        .edge_defaults
        .as_ref()
        .and_then(|d| d.interpolate.as_deref())
        .or(cfg_curve.as_deref())
        .unwrap_or("basis")
        .to_string();
    let default_edge_style = model
        .edge_defaults
        .as_ref()
        .map(|d| d.style.clone())
        .unwrap_or_default();

    let node_border_color = theme_color(effective_config_value, "nodeBorder", "#9370DB");
    let node_fill_color = theme_color(effective_config_value, "mainBkg", "#ECECFF");

    FlowchartRenderConfig {
        font_family,
        font_size,
        wrapping_width,
        node_html_labels,
        edge_html_labels,
        node_wrap_mode,
        edge_wrap_mode,
        diagram_padding,
        use_max_width,
        title_top_margin,
        node_padding,
        text_style,
        html_label_text_style,
        default_edge_interpolate,
        default_edge_style,
        node_border_color,
        node_fill_color,
    }
}

fn parse_font_size_px(v: &serde_json::Value) -> Option<f64> {
    if let Some(n) = v.as_f64() {
        return Some(n);
    }
    if let Some(n) = v.as_i64() {
        return Some(n as f64);
    }
    if let Some(n) = v.as_u64() {
        return Some(n as f64);
    }
    let s = v.as_str()?.trim();
    if s.is_empty() {
        return None;
    }
    let mut num = String::new();
    for (idx, ch) in s.chars().enumerate() {
        if ch.is_ascii_digit() {
            num.push(ch);
            continue;
        }
        if idx == 0 && (ch == '-' || ch == '+') {
            num.push(ch);
            continue;
        }
        break;
    }
    if num.trim().is_empty() {
        return None;
    }
    num.parse::<f64>().ok()
}