use std::borrow::Cow; use serde_yaml::Value; use crate::theme::{MermaidTheme, MermaidThemePreset, MermaidThemeVariables}; #[derive(Debug, Clone, Default, PartialEq)] pub struct ParsedMermaidSource<'a> { pub body: Cow<'a, str>, pub frontmatter: Option, pub config: RenderConfig, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct MermaidFrontmatter { pub title: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RenderConfig { pub theme: Option, pub theme_variables: MermaidThemeVariables, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub layout: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub look: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub security_level: Option, pub font_family: Option, pub font_size: Option, pub flowchart: FlowchartConfig, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct FlowchartConfig { pub curve: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub html_labels: Option, pub node_spacing: Option, pub rank_spacing: Option, pub padding: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub diagram_padding: Option, pub wrapping_width: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub use_max_width: Option, /// Parsed for Mermaid frontmatter compatibility, but not currently rendered. pub default_renderer: Option, } impl RenderConfig { pub fn to_mermaid_theme(&self) -> Option { if self.theme.is_none() && self.theme_variables.is_empty() { return None; } let mut theme = self.theme.unwrap_or(MermaidThemePreset::Default).to_theme(); self.theme_variables.apply_to(&mut theme); Some(theme) } pub fn font_size_px(&self) -> Option { self.font_size.as_deref().and_then(parse_font_size) } } pub fn parse_mermaid_frontmatter(source: &str) -> ParsedMermaidSource<'_> { let Some((yaml_start, yaml_end, body_start)) = frontmatter_bounds(source) else { return ParsedMermaidSource { body: Cow::Borrowed(source), frontmatter: None, config: RenderConfig::default(), }; }; let body = Cow::Owned(source[body_start..].to_string()); let yaml = &source[yaml_start..yaml_end]; let Some(value) = parse_yaml_value(yaml) else { return ParsedMermaidSource { body, frontmatter: Some(MermaidFrontmatter::default()), config: RenderConfig::default(), }; }; let frontmatter = parse_frontmatter_metadata(&value); let config = parse_render_config(&value); ParsedMermaidSource { body, frontmatter: Some(frontmatter), config, } } fn parse_yaml_value(yaml: &str) -> Option { if yaml.trim().is_empty() { return Some(Value::Null); } serde_yaml::from_str::(yaml).ok() } fn parse_frontmatter_metadata(value: &Value) -> MermaidFrontmatter { MermaidFrontmatter { title: mapping_value(value, "title").and_then(value_to_string), } } fn parse_render_config(value: &Value) -> RenderConfig { let Some(config) = mapping_value(value, "config") else { return RenderConfig::default(); }; RenderConfig { theme: mapping_value(config, "theme") .and_then(value_to_string) .and_then(|theme| MermaidThemePreset::parse(&theme)), theme_variables: parse_theme_variables(mapping_value(config, "themeVariables")), layout: mapping_value(config, "layout").and_then(value_to_string), look: mapping_value(config, "look").and_then(value_to_string), security_level: mapping_value(config, "securityLevel").and_then(value_to_string), font_family: mapping_value(config, "fontFamily").and_then(value_to_string), font_size: mapping_value(config, "fontSize").and_then(value_to_string), flowchart: parse_flowchart_config(mapping_value(config, "flowchart")), } } fn parse_theme_variables(value: Option<&Value>) -> MermaidThemeVariables { let mut variables = MermaidThemeVariables::default(); let Some(Value::Mapping(mapping)) = value else { return variables; }; for (key, value) in mapping { let Some(key) = key.as_str() else { continue; }; let Some(value) = value_to_string(value) else { continue; }; variables.apply_mermaid_alias(key, value); } variables } fn parse_flowchart_config(value: Option<&Value>) -> FlowchartConfig { let Some(flowchart) = value else { return FlowchartConfig::default(); }; FlowchartConfig { curve: mapping_value(flowchart, "curve").and_then(value_to_string), html_labels: mapping_value(flowchart, "htmlLabels").and_then(value_to_bool), node_spacing: mapping_value(flowchart, "nodeSpacing").and_then(value_to_u32), rank_spacing: mapping_value(flowchart, "rankSpacing").and_then(value_to_u32), padding: mapping_value(flowchart, "padding").and_then(value_to_u32), diagram_padding: mapping_value(flowchart, "diagramPadding").and_then(value_to_u32), wrapping_width: mapping_value(flowchart, "wrappingWidth").and_then(value_to_u32), use_max_width: mapping_value(flowchart, "useMaxWidth").and_then(value_to_bool), default_renderer: mapping_value(flowchart, "defaultRenderer").and_then(value_to_string), } } fn mapping_value<'a>(value: &'a Value, key: &str) -> Option<&'a Value> { let Value::Mapping(mapping) = value else { return None; }; mapping.get(&Value::String(key.to_string())) } fn value_to_string(value: &Value) -> Option { match value { Value::String(value) => Some(value.clone()), Value::Number(value) => Some(value.to_string()), Value::Bool(value) => Some(value.to_string()), _ => None, } } fn value_to_bool(value: &Value) -> Option { match value { Value::Bool(value) => Some(*value), Value::String(value) => match value.as_str() { "true" => Some(true), "false" => Some(false), _ => None, }, _ => None, } } fn value_to_u32(value: &Value) -> Option { match value { Value::Number(value) => value.as_u64().and_then(|value| u32::try_from(value).ok()), Value::String(value) => value.parse().ok(), _ => None, } } fn parse_font_size(value: &str) -> Option { let trimmed = value.trim(); let numeric = trimmed.strip_suffix("px").unwrap_or(trimmed).trim(); numeric .parse::() .ok() .filter(|size| size.is_finite() && *size > 0.0) } fn frontmatter_bounds(source: &str) -> Option<(usize, usize, usize)> { let mut cursor = 0; while cursor < source.len() { let end = next_line_end(source, cursor); let line = source[cursor..end].trim(); if line.is_empty() { cursor = end; continue; } if line != "---" { return None; } let yaml_start = end; let mut scan = end; while scan < source.len() { let scan_end = next_line_end(source, scan); if source[scan..scan_end].trim() == "---" { return Some((yaml_start, scan, scan_end)); } scan = scan_end; } return None; } None } fn next_line_end(source: &str, start: usize) -> usize { source[start..] .find('\n') .map(|position| start + position + 1) .unwrap_or(source.len()) }