Skip to main content

elicitation/type_graph/render/
mermaid.rs

1//! Mermaid flowchart renderer.
2//!
3//! Produces output suitable for embedding in Markdown, GitHub READMEs,
4//! and agent responses. Renders as a `graph TD` (top-down) or `graph LR`
5//! (left-right) flowchart.
6
7use crate::type_graph::builder::{NodeKind, TypeGraph};
8use crate::type_graph::render::GraphRenderer;
9
10/// Direction of the Mermaid flowchart layout.
11#[derive(Debug, Clone, Copy, Default)]
12pub enum MermaidDirection {
13    /// Top-down (default). Best for deep hierarchies.
14    #[default]
15    TopDown,
16    /// Left-right. Best for wide/flat structures.
17    LeftRight,
18}
19
20impl MermaidDirection {
21    fn as_str(self) -> &'static str {
22        match self {
23            Self::TopDown => "TD",
24            Self::LeftRight => "LR",
25        }
26    }
27}
28
29/// Renders a [`TypeGraph`] as Mermaid flowchart syntax.
30///
31/// # Example output
32///
33/// ````text
34/// graph TD
35///     ApplicationConfig["ApplicationConfig (survey)"]
36///     NetworkConfig["NetworkConfig (survey)"]
37///     ApplicationConfig -->|network| NetworkConfig
38/// ````
39#[derive(Debug, Clone)]
40pub struct MermaidRenderer {
41    /// Graph layout direction.
42    pub direction: MermaidDirection,
43    /// Include primitive and generic leaf nodes in the output.
44    /// Default: `false` — cleaner graphs for complex workflows.
45    pub include_primitives: bool,
46}
47
48impl Default for MermaidRenderer {
49    fn default() -> Self {
50        Self {
51            direction: MermaidDirection::TopDown,
52            include_primitives: false,
53        }
54    }
55}
56
57impl MermaidRenderer {
58    /// Create a renderer with default settings (top-down, no primitives).
59    pub fn new() -> Self {
60        Self::default()
61    }
62}
63
64impl GraphRenderer for MermaidRenderer {
65    fn render(&self, graph: &TypeGraph) -> String {
66        let mut out = format!("graph {}\n", self.direction.as_str());
67
68        // Declare all composite nodes with labels showing pattern.
69        let mut node_names: Vec<&str> = graph.nodes.keys().map(String::as_str).collect();
70        node_names.sort_unstable();
71
72        for name in &node_names {
73            let node = &graph.nodes[*name];
74            let skip = matches!(node.kind, NodeKind::Primitive | NodeKind::Generic)
75                && !self.include_primitives;
76            if skip {
77                continue;
78            }
79            let label = mermaid_node_label(name, node);
80            // Mermaid node ids can't contain `::` — sanitise to `__`.
81            let id = sanitize_id(name);
82            out.push_str(&format!("    {}{}\n", id, label));
83        }
84
85        // Edges.
86        for edge in &graph.edges {
87            let target_node = graph.nodes.get(&edge.to);
88            let target_is_leaf = target_node
89                .is_none_or(|n| matches!(n.kind, NodeKind::Primitive | NodeKind::Generic));
90            if target_is_leaf && !self.include_primitives {
91                continue;
92            }
93            let from_id = sanitize_id(&edge.from);
94            let to_id = sanitize_id(&edge.to);
95            let edge_label = match &edge.prompt {
96                Some(p) => format!("{}: {}", edge.label, p),
97                None => edge.label.clone(),
98            };
99            out.push_str(&format!("    {} -->|{}| {}\n", from_id, edge_label, to_id));
100        }
101
102        out
103    }
104}
105
106/// Build the Mermaid bracket label for a node, including the prompt when present.
107fn mermaid_node_label(name: &str, node: &crate::type_graph::builder::GraphNode) -> String {
108    let kind_tag = match node.kind {
109        NodeKind::Survey => "survey",
110        NodeKind::Select => "select",
111        NodeKind::Affirm => "affirm",
112        NodeKind::Primitive => return format!("[\"{}\"]", name),
113        NodeKind::Generic => return format!("(\"(generic:{})\")", name),
114    };
115    match &node.prompt {
116        Some(p) => format!("[\"{} ({})\\n'{}'\"]", name, kind_tag, p),
117        None => format!("[\"{} ({})\"]", name, kind_tag),
118    }
119}
120
121/// Replace characters invalid in Mermaid node identifiers.
122fn sanitize_id(name: &str) -> String {
123    name.replace("::", "__")
124        .replace(['<', '>', ' ', '(', ')'], "_")
125}