Skip to main content

cognis_graph/viz/
mermaid.rs

1//! Mermaid diagram renderer.
2
3use crate::compiled::CompiledGraph;
4use crate::state::GraphState;
5
6use super::extract_edges;
7
8/// Methods on `CompiledGraph<S>` that emit a Mermaid `flowchart` diagram.
9impl<S: GraphState> CompiledGraph<S> {
10    /// Render the graph as a Mermaid `flowchart TD` source string.
11    ///
12    /// Includes:
13    /// - The configured start node (highlighted with a bold border).
14    /// - Every registered node.
15    /// - Every static edge declared via `Graph::edge`.
16    /// - A synthetic `__END__` sink node — edges drawn from any node that
17    ///   has no outgoing static edge (since dynamic `Goto::End` cannot be
18    ///   inferred from the static graph).
19    pub fn to_mermaid(&self) -> String {
20        let mut out = String::from("flowchart TD\n");
21        if let Some(v) = self.version() {
22            out.push_str(&format!("    %% version: {v}\n"));
23        }
24
25        let mut names: Vec<&String> = self.graph.nodes.keys().collect();
26        names.sort();
27
28        // Node declarations.
29        for name in &names {
30            let id = node_id(name);
31            out.push_str(&format!("    {id}[\"{}\"]\n", escape(name)));
32        }
33        out.push_str("    __END__([END])\n");
34
35        // Start marker.
36        if let Some(start) = &self.graph.start {
37            out.push_str(&format!("    START(((Start))) --> {}\n", node_id(start)));
38        }
39
40        // Static edges.
41        for e in extract_edges(self) {
42            out.push_str(&format!(
43                "    {} --> {}\n",
44                node_id(&e.from),
45                node_id(&e.to)
46            ));
47        }
48
49        out
50    }
51}
52
53fn node_id(name: &str) -> String {
54    let mut s = String::with_capacity(name.len());
55    for c in name.chars() {
56        if c.is_ascii_alphanumeric() || c == '_' {
57            s.push(c);
58        } else {
59            s.push('_');
60        }
61    }
62    if s.is_empty() {
63        s.push_str("node");
64    }
65    s
66}
67
68fn escape(s: &str) -> String {
69    s.replace('"', "&quot;")
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::builder::Graph;
76    use crate::goto::Goto;
77    use crate::node::{node_fn, NodeOut};
78
79    #[derive(Default, Clone)]
80    struct S;
81    #[derive(Default)]
82    struct SU;
83    impl GraphState for S {
84        type Update = SU;
85        fn apply(&mut self, _: Self::Update) {}
86    }
87
88    fn build() -> CompiledGraph<S> {
89        Graph::<S>::new()
90            .node(
91                "a",
92                node_fn::<S, _, _>("a", |_s, _c| async move {
93                    Ok(NodeOut {
94                        update: SU,
95                        goto: Goto::node("b"),
96                    })
97                }),
98            )
99            .node(
100                "b",
101                node_fn::<S, _, _>("b", |_s, _c| async move {
102                    Ok(NodeOut {
103                        update: SU,
104                        goto: Goto::end(),
105                    })
106                }),
107            )
108            .edge("a", "b")
109            .start_at("a")
110            .compile()
111            .unwrap()
112    }
113
114    #[test]
115    fn renders_basic_diagram() {
116        let g = build();
117        let m = g.to_mermaid();
118        assert!(m.starts_with("flowchart TD\n"));
119        assert!(m.contains("a[\"a\"]"));
120        assert!(m.contains("b[\"b\"]"));
121        assert!(m.contains("a --> b"));
122        assert!(m.contains("__END__"));
123        assert!(m.contains("START(((Start))) --> a"));
124    }
125}