elicitation/type_graph/render/
mermaid.rs1use crate::type_graph::builder::{NodeKind, TypeGraph};
8use crate::type_graph::render::GraphRenderer;
9
10#[derive(Debug, Clone, Copy, Default)]
12pub enum MermaidDirection {
13 #[default]
15 TopDown,
16 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#[derive(Debug, Clone)]
40pub struct MermaidRenderer {
41 pub direction: MermaidDirection,
43 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 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 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 let id = sanitize_id(name);
82 out.push_str(&format!(" {}{}\n", id, label));
83 }
84
85 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
106fn 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
121fn sanitize_id(name: &str) -> String {
123 name.replace("::", "__")
124 .replace(['<', '>', ' ', '(', ')'], "_")
125}