1use std::{cell::RefCell, collections::HashSet, rc::Rc};
2
3use bc_components::{Digest, DigestProvider};
4
5use super::FormatContextOpt;
6use crate::{
7 EdgeType, Envelope, base::envelope::EnvelopeCase, with_format_context,
8};
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
11pub enum MermaidOrientation {
12 LeftToRight,
13 TopToBottom,
14 RightToLeft,
15 BottomToTop,
16}
17
18impl Default for MermaidOrientation {
19 fn default() -> Self { MermaidOrientation::LeftToRight }
20}
21
22impl std::fmt::Display for MermaidOrientation {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 MermaidOrientation::LeftToRight => write!(f, "LR"),
26 MermaidOrientation::TopToBottom => write!(f, "TB"),
27 MermaidOrientation::RightToLeft => write!(f, "RL"),
28 MermaidOrientation::BottomToTop => write!(f, "BT"),
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum MermaidTheme {
35 Default,
36 Neutral,
37 Dark,
38 Forest,
39 Base,
40}
41
42impl Default for MermaidTheme {
43 fn default() -> Self { MermaidTheme::Default }
44}
45
46impl std::fmt::Display for MermaidTheme {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 MermaidTheme::Default => write!(f, "default"),
50 MermaidTheme::Neutral => write!(f, "neutral"),
51 MermaidTheme::Dark => write!(f, "dark"),
52 MermaidTheme::Forest => write!(f, "forest"),
53 MermaidTheme::Base => write!(f, "base"),
54 }
55 }
56}
57
58#[derive(Clone)]
59pub struct MermaidFormatOpts<'a> {
60 hide_nodes: bool,
61 monochrome: bool,
62 theme: MermaidTheme,
63 orientation: MermaidOrientation,
64 highlighting_target: HashSet<Digest>,
65 context: FormatContextOpt<'a>,
66}
67
68impl Default for MermaidFormatOpts<'_> {
69 fn default() -> Self {
70 Self {
71 hide_nodes: false,
72 monochrome: false,
73 theme: MermaidTheme::default(),
74 orientation: MermaidOrientation::default(),
75 highlighting_target: HashSet::new(),
76 context: FormatContextOpt::Global,
77 }
78 }
79}
80
81impl<'a> MermaidFormatOpts<'a> {
82 pub fn hide_nodes(mut self, hide: bool) -> Self {
85 self.hide_nodes = hide;
86 self
87 }
88
89 pub fn monochrome(mut self, monochrome: bool) -> Self {
92 self.monochrome = monochrome;
93 self
94 }
95
96 pub fn theme(mut self, theme: MermaidTheme) -> Self {
98 self.theme = theme;
99 self
100 }
101
102 pub fn orientation(mut self, orientation: MermaidOrientation) -> Self {
105 self.orientation = orientation;
106 self
107 }
108
109 pub fn highlighting_target(mut self, target: HashSet<Digest>) -> Self {
111 self.highlighting_target = target;
112 self
113 }
114
115 pub fn context(mut self, context: FormatContextOpt<'a>) -> Self {
117 self.context = context;
118 self
119 }
120}
121
122impl Envelope {
124 pub fn mermaid_format(&self) -> String {
125 self.mermaid_format_opt(&MermaidFormatOpts::default())
126 }
127
128 pub fn mermaid_format_opt<'a>(
129 &self,
130 opts: &MermaidFormatOpts<'a>,
131 ) -> String {
132 let elements: RefCell<Vec<Rc<MermaidElement>>> =
133 RefCell::new(Vec::new());
134 let next_id = RefCell::new(0);
135 let visitor = |envelope: Self,
136 level: usize,
137 incoming_edge: EdgeType,
138 parent: Option<Rc<MermaidElement>>|
139 -> _ {
140 let id = *next_id.borrow_mut();
141 *next_id.borrow_mut() += 1;
142 let elem = Rc::new(MermaidElement::new(
143 id,
144 level,
145 envelope.clone(),
146 incoming_edge,
147 !opts.hide_nodes,
148 opts.highlighting_target.contains(&envelope.digest()),
149 parent.clone(),
150 ));
151 elements.borrow_mut().push(elem.clone());
152 Some(elem)
153 };
154 let s = self.clone();
155 s.walk(opts.hide_nodes, &visitor);
156
157 let elements = elements.borrow();
158
159 let mut element_ids: HashSet<usize> =
160 elements.iter().map(|e| e.id).collect();
161
162 let mut lines = vec![
163 format!(
164 "%%{{ init: {{ 'theme': '{}', 'flowchart': {{ 'curve': 'basis' }} }} }}%%",
165 opts.theme
166 ),
167 format!("graph {}", opts.orientation),
168 ];
169
170 let mut node_styles: Vec<String> = Vec::new();
171 let mut link_styles: Vec<String> = Vec::new();
172 let mut link_index = 0;
173
174 for element in elements.iter() {
175 let indent = " ".repeat(element.level);
176 let content = if let Some(parent) = element.parent.as_ref() {
177 let mut this_link_styles = Vec::new();
178 if !opts.monochrome {
179 if let Some(color) =
180 element.incoming_edge.link_stroke_color()
181 {
182 this_link_styles.push(format!("stroke:{}", color));
183 }
184 }
185 if element.is_highlighted && parent.is_highlighted {
186 this_link_styles.push("stroke-width:4px".to_string());
187 } else {
188 this_link_styles.push("stroke-width:2px".to_string());
189 }
190 if !this_link_styles.is_empty() {
191 link_styles.push(format!(
192 "linkStyle {} {}",
193 link_index,
194 this_link_styles.join(",")
195 ));
196 }
197 link_index += 1;
198 element.format_edge(&mut element_ids)
199 } else {
200 element.format_node(&mut element_ids)
201 };
202 let mut this_node_styles = Vec::new();
203 if !opts.monochrome {
204 let stroke_color = element.envelope.node_color();
205 this_node_styles.push(format!("stroke:{}", stroke_color));
206 }
207 if element.is_highlighted {
208 this_node_styles.push("stroke-width:6px".to_string());
209 } else {
210 this_node_styles.push("stroke-width:4px".to_string());
211 }
212 if !this_node_styles.is_empty() {
213 node_styles.push(format!(
214 "style {} {}",
215 element.id,
216 this_node_styles.join(",")
217 ));
218 }
219 lines.push(format!("{}{}", indent, content));
220 }
221
222 for style in node_styles {
223 lines.push(style);
224 }
225
226 for style in link_styles {
227 lines.push(style);
228 }
229
230 lines.join("\n")
231 }
232}
233
234#[derive(Debug)]
236struct MermaidElement {
237 id: usize,
238 level: usize,
240 envelope: Envelope,
242 incoming_edge: EdgeType,
244 show_id: bool,
246 is_highlighted: bool,
248 parent: Option<Rc<MermaidElement>>,
250}
251
252impl MermaidElement {
253 fn new(
254 id: usize,
255 level: usize,
256 envelope: Envelope,
257 incoming_edge: EdgeType,
258 show_id: bool,
259 is_highlighted: bool,
260 parent: Option<Rc<MermaidElement>>,
261 ) -> Self {
262 Self {
263 id,
264 level,
265 envelope,
266 incoming_edge,
267 show_id,
268 is_highlighted,
269 parent,
270 }
271 }
272
273 fn format_node(&self, element_ids: &mut HashSet<usize>) -> String {
274 if element_ids.contains(&self.id) {
275 element_ids.remove(&self.id);
276 let mut lines: Vec<String> = Vec::new();
277 let summary = with_format_context!(|ctx| {
278 format!(
279 r#"{}"#,
280 self.envelope.summary(20, ctx).replace('"', """)
281 )
282 });
283 lines.push(summary);
284 if self.show_id {
285 let id = self.envelope.digest().short_description();
286 lines.push(id);
287 }
288 let lines = lines.join("<br>");
289 let (frame_l, frame_r) = self.envelope.mermaid_frame();
290 let id = self.id;
291 format!(r#"{id}{frame_l}"{lines}"{frame_r}"#)
292 } else {
293 format!("{}", self.id)
294 }
295 }
296
297 fn format_edge(&self, element_ids: &mut HashSet<usize>) -> String {
298 let parent = self.parent.as_ref().unwrap();
299 let arrow = if let Some(label) = self.incoming_edge.label() {
300 format!("-- {} -->", label)
301 } else {
302 "-->".to_string()
303 };
304 format!(
305 "{} {} {}",
306 parent.format_node(element_ids),
307 arrow,
308 self.format_node(element_ids)
309 )
310 }
311}
312
313impl Envelope {
314 #[rustfmt::skip]
315 fn mermaid_frame(&self) -> (&str, &str) {
316 match self.case() {
317 EnvelopeCase::Node { .. } => ("((", "))"),
318 EnvelopeCase::Leaf { .. } => ("[", "]"),
319 EnvelopeCase::Wrapped { .. } => ("[/", "\\]"),
320 EnvelopeCase::Assertion(..) => ("([", "])"),
321 EnvelopeCase::Elided(..) => ("{{", "}}"),
322 #[cfg(feature = "known_value")]
323 EnvelopeCase::KnownValue { .. } => ("[/", "/]"),
324 #[cfg(feature = "encrypt")]
325 EnvelopeCase::Encrypted(..) => (">", "]"),
326 #[cfg(feature = "compress")]
327 EnvelopeCase::Compressed(..) => ("[[", "]]"),
328 }
329 }
330
331 #[rustfmt::skip]
332 fn node_color(&self) -> &'static str {
333 match self.case() {
334 EnvelopeCase::Node { .. } => "red",
335 EnvelopeCase::Leaf { .. } => "teal",
336 EnvelopeCase::Wrapped { .. } => "blue",
337 EnvelopeCase::Assertion(..) => "green",
338 EnvelopeCase::Elided(..) => "gray",
339 #[cfg(feature = "known_value")]
340 EnvelopeCase::KnownValue { .. } => "goldenrod",
341 #[cfg(feature = "encrypt")]
342 EnvelopeCase::Encrypted(..) => "coral",
343 #[cfg(feature = "compress")]
344 EnvelopeCase::Compressed(..) => "purple",
345 }
346 }
347}
348
349impl EdgeType {
350 pub fn link_stroke_color(&self) -> Option<&'static str> {
351 match self {
352 EdgeType::Subject | EdgeType::Wrapped => Some("red"),
353 EdgeType::Predicate => Some("green"),
354 EdgeType::Object => Some("blue"),
355 _ => None,
356 }
357 }
358}