merman-render 0.5.0

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use serde_json::Value;

pub(crate) fn value_at<'a>(cfg: &'a Value, path: &[&str]) -> Option<&'a Value> {
    let mut cur = cfg;
    for key in path {
        cur = cur.get(*key)?;
    }
    Some(cur)
}

pub(crate) fn json_f64(value: &Value) -> Option<f64> {
    value
        .as_f64()
        .or_else(|| value.as_i64().map(|n| n as f64))
        .or_else(|| value.as_u64().map(|n| n as f64))
        .or_else(|| {
            let n = value.as_str()?.trim().parse::<f64>().ok()?;
            n.is_finite().then_some(n)
        })
}

pub(crate) fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
    value_at(cfg, path).and_then(json_f64)
}

pub(crate) fn config_f64_or(cfg: &Value, path: &[&str], default: f64) -> f64 {
    config_f64(cfg, path).unwrap_or(default)
}

pub(crate) fn json_f64_css_px(value: &Value) -> Option<f64> {
    json_f64(value).or_else(|| value.as_str().and_then(parse_css_px_to_f64))
}

pub(crate) fn config_f64_css_px(cfg: &Value, path: &[&str]) -> Option<f64> {
    value_at(cfg, path).and_then(json_f64_css_px)
}

fn parse_css_px_to_f64(text: &str) -> Option<f64> {
    let text = text.trim().trim_end_matches(';').trim();
    let text = text.trim_end_matches("!important").trim();
    let text = text.strip_suffix("px").unwrap_or(text).trim();
    let value = text.parse::<f64>().ok()?;
    value.is_finite().then_some(value)
}

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

    #[test]
    fn json_f64_accepts_json_numbers_and_plain_numeric_strings() {
        assert_eq!(json_f64(&json!(100)), Some(100.0));
        assert_eq!(json_f64(&json!(70.5)), Some(70.5));
        assert_eq!(json_f64(&json!("100")), Some(100.0));
        assert_eq!(json_f64(&json!(" 70.5 ")), Some(70.5));
    }

    #[test]
    fn json_f64_rejects_css_or_nonfinite_strings() {
        assert_eq!(json_f64(&json!("100px")), None);
        assert_eq!(json_f64(&json!("NaN")), None);
        assert_eq!(json_f64(&json!("inf")), None);
    }

    #[test]
    fn config_f64_walks_paths_and_accepts_yaml_string_numbers() {
        let cfg = json!({
            "flowchart": {
                "rankSpacing": "100",
                "nodeSpacing": "70.5"
            }
        });

        assert_eq!(config_f64(&cfg, &["flowchart", "rankSpacing"]), Some(100.0));
        assert_eq!(config_f64(&cfg, &["flowchart", "nodeSpacing"]), Some(70.5));
        assert_eq!(config_f64(&cfg, &["missing", "rankSpacing"]), None);
    }

    #[test]
    fn json_f64_css_px_accepts_plain_and_css_numeric_strings() {
        assert_eq!(json_f64_css_px(&json!("24")), Some(24.0));
        assert_eq!(json_f64_css_px(&json!("24px")), Some(24.0));
        assert_eq!(json_f64_css_px(&json!("24px !important;")), Some(24.0));
        assert_eq!(json_f64_css_px(&json!("24pt")), None);
        assert_eq!(json_f64_css_px(&json!("NaNpx")), None);
    }
}