1use std::borrow::Cow;
2use std::fmt::Write;
3
4use super::render::{HydroEdgeType, HydroGraphWrite, HydroNodeType, IndentedGraphWriter};
5
6pub fn escape_mermaid(string: &str) -> String {
8 string
9 .replace('&', "&")
10 .replace('<', "<")
11 .replace('>', ">")
12 .replace('"', """)
13 .replace('#', "#")
14 .replace('\n', "<br>")
15 .replace("`", "`")
17 .replace('(', "(")
19 .replace(')', ")")
20 .replace('|', "|")
22}
23
24pub struct HydroMermaid<W> {
26 base: IndentedGraphWriter<W>,
27 link_count: usize,
28}
29
30impl<W> HydroMermaid<W> {
31 pub fn new(write: W) -> Self {
32 Self {
33 base: IndentedGraphWriter::new(write),
34 link_count: 0,
35 }
36 }
37
38 pub fn new_with_config(write: W, config: &super::render::HydroWriteConfig) -> Self {
39 Self {
40 base: IndentedGraphWriter::new_with_config(write, config),
41 link_count: 0,
42 }
43 }
44}
45
46impl<W> HydroGraphWrite for HydroMermaid<W>
47where
48 W: Write,
49{
50 type Err = super::render::GraphWriteError;
51
52 fn write_prologue(&mut self) -> Result<(), Self::Err> {
53 writeln!(
54 self.base.write,
55 "{b:i$}%%{{init:{{'theme':'base','themeVariables':{{'clusterBkg':'#fafafa','clusterBorder':'#e0e0e0'}},'elk':{{'algorithm':'mrtree','elk.direction':'DOWN','elk.layered.spacing.nodeNodeBetweenLayers':'30'}}}}}}%%
56{b:i$}graph TD
57{b:i$}classDef sourceClass fill:#8dd3c7,stroke:#86c8bd,text-align:left,white-space:pre
58{b:i$}classDef transformClass fill:#ffffb3,stroke:#f5f5a8,text-align:left,white-space:pre
59{b:i$}classDef joinClass fill:#bebada,stroke:#b5b1cf,text-align:left,white-space:pre
60{b:i$}classDef aggClass fill:#fb8072,stroke:#ee796b,text-align:left,white-space:pre
61{b:i$}classDef networkClass fill:#80b1d3,stroke:#79a8c8,text-align:left,white-space:pre
62{b:i$}classDef sinkClass fill:#fdb462,stroke:#f0aa5b,text-align:left,white-space:pre
63{b:i$}classDef teeClass fill:#b3de69,stroke:#aad362,text-align:left,white-space:pre
64{b:i$}linkStyle default stroke:#666666",
65 b = "",
66 i = self.base.indent
67 )?;
68 Ok(())
69 }
70
71 fn write_node_definition(
72 &mut self,
73 node_id: usize,
74 node_label: &super::render::NodeLabel,
75 node_type: HydroNodeType,
76 _location_id: Option<usize>,
77 _location_type: Option<&str>,
78 ) -> Result<(), Self::Err> {
79 let class_str = match node_type {
80 HydroNodeType::Source => "sourceClass",
81 HydroNodeType::Transform => "transformClass",
82 HydroNodeType::Join => "joinClass",
83 HydroNodeType::Aggregation => "aggClass",
84 HydroNodeType::Network => "networkClass",
85 HydroNodeType::Sink => "sinkClass",
86 HydroNodeType::Tee => "teeClass",
87 };
88
89 let (lbracket, rbracket) = match node_type {
90 HydroNodeType::Source => ("[[", "]]"),
91 HydroNodeType::Sink => ("[/", "/]"),
92 HydroNodeType::Network => ("[[", "]]"),
93 HydroNodeType::Tee => ("(", ")"),
94 _ => ("[", "]"),
95 };
96
97 let full_label = match node_label {
99 super::render::NodeLabel::Static(s) => s.clone(),
100 super::render::NodeLabel::WithExprs { op_name, exprs } => {
101 if exprs.is_empty() {
102 format!("{}()", op_name)
103 } else {
104 let expr_strs: Vec<String> = exprs.iter().map(|e| e.to_string()).collect();
106 format!("{}({})", op_name, expr_strs.join(", "))
107 }
108 }
109 };
110
111 let display_label = if self.base.config.use_short_labels {
113 super::render::extract_short_label(&full_label)
114 } else {
115 full_label
116 };
117
118 let label = format!(
119 r#"n{node_id}{lbracket}"{escaped_label}"{rbracket}:::{class}"#,
120 escaped_label = escape_mermaid(&display_label),
121 class = class_str,
122 );
123
124 writeln!(
125 self.base.write,
126 "{b:i$}{label}",
127 b = "",
128 i = self.base.indent
129 )?;
130 Ok(())
131 }
132
133 fn write_edge(
134 &mut self,
135 src_id: usize,
136 dst_id: usize,
137 edge_type: HydroEdgeType,
138 label: Option<&str>,
139 ) -> Result<(), Self::Err> {
140 let arrow_style = match edge_type {
141 HydroEdgeType::Stream => "-->",
142 HydroEdgeType::Persistent => "==>",
143 HydroEdgeType::Network => "-.->",
144 HydroEdgeType::Cycle => "--o",
145 };
146
147 writeln!(
149 self.base.write,
150 "{b:i$}n{src}{arrow}{label}n{dst}",
151 src = src_id,
152 arrow = arrow_style,
153 label = if let Some(label) = label {
154 Cow::Owned(format!("|{}|", escape_mermaid(label)))
155 } else {
156 Cow::Borrowed("")
157 },
158 dst = dst_id,
159 b = "",
160 i = self.base.indent,
161 )?;
162
163 if !matches!(edge_type, HydroEdgeType::Stream) {
165 writeln!(
166 self.base.write,
167 "{b:i$}linkStyle {} stroke:{}",
168 self.link_count,
169 match edge_type {
170 HydroEdgeType::Persistent => "#008800",
171 HydroEdgeType::Network => "#880088",
172 HydroEdgeType::Cycle => "#ff0000",
173 HydroEdgeType::Stream => "#666666", },
175 b = "",
176 i = self.base.indent,
177 )?;
178 }
179
180 self.link_count += 1;
181 Ok(())
182 }
183
184 fn write_location_start(
185 &mut self,
186 location_id: usize,
187 location_type: &str,
188 ) -> Result<(), Self::Err> {
189 writeln!(
190 self.base.write,
191 "{b:i$}subgraph loc_{id} [\"{location_type} {id}\"]",
192 id = location_id,
193 b = "",
194 i = self.base.indent,
195 )?;
196 self.base.indent += 4;
197 Ok(())
198 }
199
200 fn write_node(&mut self, node_id: usize) -> Result<(), Self::Err> {
201 writeln!(
202 self.base.write,
203 "{b:i$}n{node_id}",
204 b = "",
205 i = self.base.indent
206 )
207 }
208
209 fn write_location_end(&mut self) -> Result<(), Self::Err> {
210 self.base.indent -= 4;
211 writeln!(self.base.write, "{b:i$}end", b = "", i = self.base.indent)
212 }
213
214 fn write_epilogue(&mut self) -> Result<(), Self::Err> {
215 Ok(())
216 }
217}
218
219#[cfg(feature = "build")]
221pub fn open_browser(
222 built_flow: &crate::builder::built::BuiltFlow,
223) -> Result<(), Box<dyn std::error::Error>> {
224 let config = super::render::HydroWriteConfig {
225 show_metadata: false,
226 show_location_groups: true,
227 use_short_labels: true, process_id_name: built_flow.process_id_name().clone(),
229 cluster_id_name: built_flow.cluster_id_name().clone(),
230 external_id_name: built_flow.external_id_name().clone(),
231 };
232
233 crate::graph::debug::open_mermaid(built_flow.ir(), Some(config))?;
235
236 Ok(())
237}