use std::fmt::Display;
use crate::graph::capability::Bigraph;
use crate::graph::Graph;
pub fn to_mermaid<G>(graph: &G) -> String
where
G: Graph + Bigraph + ?Sized,
G::Node: Display,
G::Edge: Display,
for<'scope> crate::graph::context::Context<'scope, G>: Graph<Node = G::Node, Edge = G::Edge>,
{
let connector = if G::DIRECTED { "-->" } else { "---" };
graph.scope(|ctx| {
let mut out = String::from("flowchart LR\n");
for n in ctx.node_indices() {
let label = escape_label(&ctx.node(n).to_string());
out.push_str(&format!(" {}[\"{}\"]\n", n, label));
}
for e in ctx.edge_indices() {
let mut it = ctx.endpoints(e).into_iter();
let from = it.next().unwrap();
let to = it.next().unwrap();
let label = escape_label(&ctx.edge(e).to_string());
out.push_str(&format!(" {} {}|{}| {}\n", from, connector, label, to));
}
out
})
}
fn escape_label(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'#' => out.push_str("#35;"),
'"' => out.push_str("#34;"),
'|' => out.push_str("#124;"),
'\n' | '\r' => out.push(' '),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::{escape_label, to_mermaid};
use crate::graph::Graph;
use crate::BTreeGraph;
#[test]
fn mermaid_empty_graph() {
let g = BTreeGraph::<u32, u32>::default();
assert_eq!(to_mermaid(&g), "flowchart LR\n");
}
#[test]
fn mermaid_btreegraph_renders_key_indices() {
let mut g = BTreeGraph::<u32, u32>::default();
g.insert_node(10).unwrap();
g.insert_node(20).unwrap();
g.insert_edge(99, [10, 20]).unwrap();
let src = to_mermaid(&g);
assert!(src.starts_with("flowchart LR\n"));
assert!(src.contains(" 10[\"10\"]\n"));
assert!(src.contains(" 20[\"20\"]\n"));
assert!(src.contains(" 10 -->|99| 20\n"));
}
#[test]
fn mermaid_btreegraph_keeps_sorted_key_order() {
let mut g = BTreeGraph::<u32, u32>::default();
g.insert_node(30).unwrap();
g.insert_node(10).unwrap();
g.insert_node(20).unwrap();
g.insert_edge(2, [20, 30]).unwrap();
g.insert_edge(1, [10, 20]).unwrap();
let src = to_mermaid(&g);
let expected = concat!(
"flowchart LR\n",
" 10[\"10\"]\n",
" 20[\"20\"]\n",
" 30[\"30\"]\n",
" 10 -->|1| 20\n",
" 20 -->|2| 30\n",
);
assert_eq!(src, expected);
}
#[test]
fn mermaid_undirected_uses_open_links() {
let mut g = BTreeGraph::<u32, u32>::default();
g.insert_node(10).unwrap();
g.insert_node(20).unwrap();
g.insert_edge(99, [10, 20]).unwrap();
let src = to_mermaid(&g.undirected());
assert!(src.contains(" 10[\"10\"]\n"), "{src}");
assert!(src.contains(" 20[\"20\"]\n"), "{src}");
assert!(src.contains(" 10 ---|99| 20\n"), "{src}");
assert!(!src.contains("-->"), "{src}");
}
#[test]
fn mermaid_shows_escaped_data() {
use crate::VecGraph;
let mut g = VecGraph::<&str, &str>::default();
g.push("A \"x\"").unwrap();
g.push("B|C").unwrap();
g.push_edge("e#1", [0, 1]).unwrap();
let src = to_mermaid(&g);
assert!(src.contains(" 0[\"A #34;x#34;\"]\n"), "{src}");
assert!(src.contains(" 1[\"B#124;C\"]\n"), "{src}");
assert!(src.contains(" 0 -->|e#35;1| 1\n"), "{src}");
}
#[test]
fn escape_label_handles_breaking_chars() {
assert_eq!(escape_label("plain"), "plain");
assert_eq!(escape_label("a\"b"), "a#34;b");
assert_eq!(escape_label("a|b"), "a#124;b");
assert_eq!(escape_label("a#b"), "a#35;b");
assert_eq!(escape_label("\""), "#34;");
assert_eq!(escape_label("line1\nline2"), "line1 line2");
}
}