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