1use crate::config::config_f64;
2use crate::math::MathRenderer;
3use crate::model::{
4 FlowchartV2Layout, LayoutCluster, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint,
5};
6use crate::text::{TextMeasurer, TextStyle, WrapMode};
7use crate::{Error, Result};
8use dugong::graphlib::{Graph, GraphOptions};
9use dugong::{EdgeLabel, GraphLabel, LabelPos, NodeLabel, RankDir};
10use merman_core::MermaidConfig;
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13
14use super::label::compute_bounds;
15use super::node::{NodeLayoutDimensionsRequest, node_layout_dimensions};
16use super::{FlowEdge, FlowSubgraph, FlowchartV2Model};
17use super::{
18 FlowchartLabelMetricsRequest, flowchart_effective_html_labels,
19 flowchart_effective_node_html_labels, flowchart_effective_text_style_for_classes,
20 flowchart_effective_text_style_for_node_classes, flowchart_html_label_measurement_base_style,
21 flowchart_label_metrics_for_layout, flowchart_node_has_span_css_height_parity,
22};
23
24fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
25 let mut cur = cfg;
26 for key in path {
27 cur = cur.get(*key)?;
28 }
29 cur.as_str().map(|s| s.to_string())
30}
31
32fn flowchart_svg_plain_computed_width_px(
33 measurer: &dyn TextMeasurer,
34 plain: &str,
35 style: &TextStyle,
36 max_width_px: Option<f64>,
37) -> f64 {
38 let wrapped_lines =
39 crate::text::wrap_svg_text_lines_by_measurement(measurer, plain, style, max_width_px, true);
40 let mut width: f64 = 0.0;
41 for line in wrapped_lines {
42 width = width.max(measurer.measure_svg_text_computed_length_px(line.trim_end(), style));
43 }
44 crate::text::round_to_1_64_px(width)
45}
46
47fn rank_dir_from_flow(direction: &str) -> RankDir {
48 match direction.trim().to_uppercase().as_str() {
49 "TB" | "TD" => RankDir::TB,
50 "BT" => RankDir::BT,
51 "LR" => RankDir::LR,
52 "RL" => RankDir::RL,
53 _ => RankDir::TB,
54 }
55}
56
57fn normalize_dir(s: &str) -> String {
58 s.trim().to_uppercase()
59}
60
61fn toggled_dir(parent: &str) -> String {
62 let parent = normalize_dir(parent);
63 if parent == "TB" || parent == "TD" {
64 "LR".to_string()
65 } else {
66 "TB".to_string()
67 }
68}
69
70fn flow_dir_from_rankdir(rankdir: RankDir) -> &'static str {
71 match rankdir {
72 RankDir::TB => "TB",
73 RankDir::BT => "BT",
74 RankDir::LR => "LR",
75 RankDir::RL => "RL",
76 }
77}
78
79fn effective_cluster_dir(sg: &FlowSubgraph, parent_dir: &str, inherit_dir: bool) -> String {
80 if let Some(dir) = sg.dir.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
81 return normalize_dir(dir);
82 }
83 if inherit_dir {
84 return normalize_dir(parent_dir);
85 }
86 toggled_dir(parent_dir)
87}
88
89fn compute_effective_dir_by_id(
90 subgraphs_by_id: &HashMap<String, FlowSubgraph>,
91 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
92 diagram_dir: &str,
93 inherit_dir: bool,
94) -> HashMap<String, String> {
95 fn compute_one(
96 id: &str,
97 subgraphs_by_id: &HashMap<String, FlowSubgraph>,
98 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
99 diagram_dir: &str,
100 inherit_dir: bool,
101 visiting: &mut std::collections::HashSet<String>,
102 memo: &mut HashMap<String, String>,
103 ) -> String {
104 if let Some(dir) = memo.get(id) {
105 return dir.clone();
106 }
107 if !visiting.insert(id.to_string()) {
108 let dir = toggled_dir(diagram_dir);
109 memo.insert(id.to_string(), dir.clone());
110 return dir;
111 }
112
113 let parent_dir = g
114 .parent(id)
115 .and_then(|p| subgraphs_by_id.contains_key(p).then_some(p.to_string()))
116 .map(|p| {
117 compute_one(
118 &p,
119 subgraphs_by_id,
120 g,
121 diagram_dir,
122 inherit_dir,
123 visiting,
124 memo,
125 )
126 })
127 .unwrap_or_else(|| normalize_dir(diagram_dir));
128
129 let dir = subgraphs_by_id
130 .get(id)
131 .map(|sg| effective_cluster_dir(sg, &parent_dir, inherit_dir))
132 .unwrap_or_else(|| toggled_dir(&parent_dir));
133
134 memo.insert(id.to_string(), dir.clone());
135 let _ = visiting.remove(id);
136 dir
137 }
138
139 let mut memo: HashMap<String, String> = HashMap::new();
140 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
141 for id in subgraphs_by_id.keys() {
142 let _ = compute_one(
143 id,
144 subgraphs_by_id,
145 g,
146 diagram_dir,
147 inherit_dir,
148 &mut visiting,
149 &mut memo,
150 );
151 }
152 memo
153}
154
155fn dir_to_rankdir(dir: &str) -> RankDir {
156 match normalize_dir(dir).as_str() {
157 "TB" | "TD" => RankDir::TB,
158 "BT" => RankDir::BT,
159 "LR" => RankDir::LR,
160 "RL" => RankDir::RL,
161 _ => RankDir::TB,
162 }
163}
164
165fn edge_label_is_non_empty(edge: &FlowEdge) -> bool {
166 edge.label
167 .as_deref()
168 .map(|s| !s.trim().is_empty())
169 .unwrap_or(false)
170}
171
172fn lowest_common_parent(
173 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
174 a: &str,
175 b: &str,
176) -> Option<String> {
177 if !g.options().compound {
178 return None;
179 }
180
181 let mut ancestors: std::collections::HashSet<String> = std::collections::HashSet::new();
182 let mut cur = g.parent(a);
183 while let Some(p) = cur {
184 ancestors.insert(p.to_string());
185 cur = g.parent(p);
186 }
187
188 let mut cur = g.parent(b);
189 while let Some(p) = cur {
190 if ancestors.contains(p) {
191 return Some(p.to_string());
192 }
193 cur = g.parent(p);
194 }
195
196 None
197}
198
199fn extract_descendants(id: &str, g: &Graph<NodeLabel, EdgeLabel, GraphLabel>) -> Vec<String> {
200 let mut out: Vec<String> = Vec::new();
201 let mut visited: HashSet<String> = HashSet::new();
202 let mut stack: Vec<String> = g.children(id).iter().map(|s| s.to_string()).collect();
203 while let Some(node) = stack.pop() {
204 if !visited.insert(node.clone()) {
205 continue;
206 }
207 for child in g.children(&node) {
208 stack.push(child.to_string());
209 }
210 out.push(node);
211 }
212 out
213}
214
215fn edge_in_cluster(
216 edge: &dugong::graphlib::EdgeKey,
217 cluster_id: &str,
218 descendants: &std::collections::HashMap<String, Vec<String>>,
219) -> bool {
220 if edge.v == cluster_id || edge.w == cluster_id {
221 return false;
222 }
223 let Some(cluster_desc) = descendants.get(cluster_id) else {
224 return false;
225 };
226 cluster_desc.contains(&edge.v) || cluster_desc.contains(&edge.w)
227}
228
229#[derive(Debug, Clone)]
230struct FlowchartClusterDbEntry {
231 anchor_id: String,
232 external_connections: bool,
233}
234
235fn flowchart_find_common_edges(
236 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
237 id1: &str,
238 id2: &str,
239) -> Vec<(String, String)> {
240 let edges1: Vec<(String, String)> = graph
241 .edge_keys()
242 .into_iter()
243 .filter(|edge| edge.v == id1 || edge.w == id1)
244 .map(|edge| (edge.v, edge.w))
245 .collect();
246 let edges2: Vec<(String, String)> = graph
247 .edge_keys()
248 .into_iter()
249 .filter(|edge| edge.v == id2 || edge.w == id2)
250 .map(|edge| (edge.v, edge.w))
251 .collect();
252
253 let edges1_prim: Vec<(String, String)> = edges1
254 .into_iter()
255 .map(|(v, w)| {
256 (
257 if v == id1 { id2.to_string() } else { v },
258 if w == id1 { id1.to_string() } else { w },
261 )
262 })
263 .collect();
264
265 let mut out = Vec::new();
266 for e1 in edges1_prim {
267 if edges2.contains(&e1) {
268 out.push(e1);
269 }
270 }
271 out
272}
273
274fn flowchart_find_non_cluster_child(
275 id: &str,
276 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
277 cluster_id: &str,
278) -> Option<String> {
279 let children = graph.children(id);
280 if children.is_empty() {
281 return Some(id.to_string());
282 }
283 let mut reserve: Option<String> = None;
284 let mut visited: HashSet<String> = HashSet::new();
285 let mut stack: Vec<String> = children.iter().rev().map(|s| s.to_string()).collect();
286 while let Some(node) = stack.pop() {
287 if !visited.insert(node.clone()) {
288 continue;
289 }
290 let children = graph.children(&node);
291 if !children.is_empty() {
292 for child in children.iter().rev() {
293 stack.push(child.to_string());
294 }
295 continue;
296 }
297 let common_edges = flowchart_find_common_edges(graph, cluster_id, &node);
298 if !common_edges.is_empty() {
299 reserve = Some(node);
300 } else {
301 return Some(node);
302 }
303 }
304 reserve
305}
306
307fn adjust_flowchart_clusters_and_edges(
308 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
309) -> std::collections::HashMap<String, bool> {
310 use serde_json::Value;
311
312 fn is_descendant(
313 node_id: &str,
314 cluster_id: &str,
315 descendants: &std::collections::HashMap<String, Vec<String>>,
316 ) -> bool {
317 descendants
318 .get(cluster_id)
319 .is_some_and(|ds| ds.iter().any(|n| n == node_id))
320 }
321
322 let mut descendants: std::collections::HashMap<String, Vec<String>> =
323 std::collections::HashMap::new();
324 let mut cluster_db: std::collections::HashMap<String, FlowchartClusterDbEntry> =
325 std::collections::HashMap::new();
326
327 for id in graph.node_ids() {
328 if graph.children(&id).is_empty() {
329 continue;
330 }
331 descendants.insert(id.clone(), extract_descendants(&id, graph));
332 let anchor_id =
333 flowchart_find_non_cluster_child(&id, graph, &id).unwrap_or_else(|| id.clone());
334 cluster_db.insert(
335 id,
336 FlowchartClusterDbEntry {
337 anchor_id,
338 external_connections: false,
339 },
340 );
341 }
342
343 for id in cluster_db.keys().cloned().collect::<Vec<_>>() {
344 if graph.children(&id).is_empty() {
345 continue;
346 }
347 let mut has_external = false;
348 for e in graph.edges() {
349 let d1 = is_descendant(e.v.as_str(), id.as_str(), &descendants);
350 let d2 = is_descendant(e.w.as_str(), id.as_str(), &descendants);
351 if d1 ^ d2 {
352 has_external = true;
353 break;
354 }
355 }
356 if let Some(entry) = cluster_db.get_mut(&id) {
357 entry.external_connections = has_external;
358 }
359 }
360
361 for id in cluster_db.keys().cloned().collect::<Vec<_>>() {
362 let Some(non_cluster_child) = cluster_db.get(&id).map(|e| e.anchor_id.clone()) else {
363 continue;
364 };
365 let parent = graph.parent(&non_cluster_child);
366 if parent.is_some_and(|p| p != id.as_str())
367 && parent.is_some_and(|p| cluster_db.contains_key(p))
368 && parent.is_some_and(|p| !cluster_db.get(p).is_some_and(|e| e.external_connections))
369 {
370 if let Some(p) = parent {
371 if let Some(entry) = cluster_db.get_mut(&id) {
372 entry.anchor_id = p.to_string();
373 }
374 }
375 }
376 }
377
378 fn get_anchor_id(
379 id: &str,
380 cluster_db: &std::collections::HashMap<String, FlowchartClusterDbEntry>,
381 ) -> String {
382 let Some(entry) = cluster_db.get(id) else {
383 return id.to_string();
384 };
385 if !entry.external_connections {
386 return id.to_string();
387 }
388 entry.anchor_id.clone()
389 }
390
391 let edge_keys = graph.edge_keys();
392 for ek in edge_keys {
393 if !cluster_db.contains_key(&ek.v) && !cluster_db.contains_key(&ek.w) {
394 continue;
395 }
396
397 let Some(mut edge_label) = graph.edge_by_key(&ek).cloned() else {
398 continue;
399 };
400
401 let v = get_anchor_id(&ek.v, &cluster_db);
402 let w = get_anchor_id(&ek.w, &cluster_db);
403
404 let _ = graph.remove_edge_key(&ek);
408
409 if v != ek.v {
410 if let Some(parent) = graph.parent(&v) {
411 if let Some(entry) = cluster_db.get_mut(parent) {
412 entry.external_connections = true;
413 }
414 }
415 edge_label
416 .extras
417 .insert("fromCluster".to_string(), Value::String(ek.v.clone()));
418 }
419
420 if w != ek.w {
421 if let Some(parent) = graph.parent(&w) {
422 if let Some(entry) = cluster_db.get_mut(parent) {
423 entry.external_connections = true;
424 }
425 }
426 edge_label
427 .extras
428 .insert("toCluster".to_string(), Value::String(ek.w.clone()));
429 }
430
431 graph.set_edge_named(v, w, ek.name, Some(edge_label));
432 }
433
434 cluster_db
435 .into_iter()
436 .map(|(id, entry)| (id, entry.external_connections))
437 .collect()
438}
439
440fn copy_cluster(
441 cluster_id: &str,
442 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
443 new_graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
444 root_id: &str,
445 descendants: &std::collections::HashMap<String, Vec<String>>,
446) {
447 let mut nodes: Vec<String> = graph
448 .children(cluster_id)
449 .iter()
450 .map(|s| s.to_string())
451 .collect();
452 if cluster_id != root_id {
453 nodes.push(cluster_id.to_string());
454 }
455
456 for node in nodes {
457 if !graph.has_node(&node) {
458 continue;
459 }
460
461 if !graph.children(&node).is_empty() {
462 copy_cluster(&node, graph, new_graph, root_id, descendants);
463 } else {
464 let data = graph.node(&node).cloned().unwrap_or_default();
465 new_graph.set_node(node.clone(), data);
466
467 if let Some(parent) = graph.parent(&node) {
468 if parent != root_id {
469 new_graph.set_parent(node.clone(), parent.to_string());
470 }
471 }
472 if cluster_id != root_id && node != cluster_id {
473 new_graph.set_parent(node.clone(), cluster_id.to_string());
474 }
475
476 for ek in graph.edge_keys() {
487 if !edge_in_cluster(&ek, root_id, descendants) {
488 continue;
489 }
490 if new_graph.has_edge(&ek.v, &ek.w, ek.name.as_deref()) {
491 continue;
492 }
493 let Some(lbl) = graph.edge_by_key(&ek).cloned() else {
494 continue;
495 };
496 new_graph.set_edge_named(ek.v, ek.w, ek.name, Some(lbl));
497 }
498 }
499
500 let _ = graph.remove_node(&node);
501 }
502}
503
504fn extract_clusters_recursively(
505 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
506 subgraphs_by_id: &std::collections::HashMap<String, FlowSubgraph>,
507 external_connections_by_id: &std::collections::HashMap<String, bool>,
508 extracted: &mut std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
509 _depth: usize,
510) {
511 struct ExtractFrame {
512 id: String,
513 graph: Graph<NodeLabel, EdgeLabel, GraphLabel>,
514 expanded: bool,
515 }
516
517 fn extract_one_level(
518 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
519 subgraphs_by_id: &std::collections::HashMap<String, FlowSubgraph>,
520 external_connections_by_id: &std::collections::HashMap<String, bool>,
521 ) -> Vec<(String, Graph<NodeLabel, EdgeLabel, GraphLabel>)> {
522 let node_ids = graph.node_ids();
523 let mut descendants: std::collections::HashMap<String, Vec<String>> =
524 std::collections::HashMap::new();
525 for id in &node_ids {
526 if graph.children(id).is_empty() {
527 continue;
528 }
529 descendants.insert(id.clone(), extract_descendants(id, graph));
530 }
531
532 let mut extracted_here: Vec<(String, Graph<NodeLabel, EdgeLabel, GraphLabel>)> = Vec::new();
533
534 let candidates: Vec<String> = node_ids
535 .into_iter()
536 .filter(|id| graph.has_node(id))
537 .filter(|id| !graph.children(id).is_empty())
538 .filter(|id| {
546 external_connections_by_id
547 .get(id.as_str())
548 .is_some_and(|external| !external)
549 })
550 .collect();
551
552 for id in candidates {
553 if !graph.has_node(&id) {
554 continue;
555 }
556 if graph.children(&id).is_empty() {
557 continue;
558 }
559
560 let mut cluster_graph: Graph<NodeLabel, EdgeLabel, GraphLabel> =
561 Graph::new(GraphOptions {
562 multigraph: true,
563 compound: true,
564 directed: true,
565 });
566
567 let dir = subgraphs_by_id
571 .get(&id)
572 .and_then(|sg| sg.dir.as_deref())
573 .map(str::trim)
574 .filter(|s| !s.is_empty())
575 .map(normalize_dir)
576 .unwrap_or_else(|| toggled_dir(flow_dir_from_rankdir(graph.graph().rankdir)));
577
578 cluster_graph.set_graph(GraphLabel {
579 rankdir: dir_to_rankdir(&dir),
580 nodesep: 50.0,
592 ranksep: 50.0,
593 marginx: 8.0,
594 marginy: 8.0,
595 acyclicer: None,
596 ..Default::default()
597 });
598
599 copy_cluster(&id, graph, &mut cluster_graph, &id, &descendants);
600 extracted_here.push((id, cluster_graph));
601 }
602
603 extracted_here
604 }
605
606 let extracted_here = extract_one_level(graph, subgraphs_by_id, external_connections_by_id);
607 let mut stack: Vec<ExtractFrame> = extracted_here
608 .into_iter()
609 .rev()
610 .map(|(id, graph)| ExtractFrame {
611 id,
612 graph,
613 expanded: false,
614 })
615 .collect();
616
617 while let Some(mut frame) = stack.pop() {
618 if frame.expanded {
619 extracted.insert(frame.id, frame.graph);
620 continue;
621 }
622
623 let children = extract_one_level(
624 &mut frame.graph,
625 subgraphs_by_id,
626 external_connections_by_id,
627 );
628 frame.expanded = true;
629 stack.push(frame);
630 stack.extend(children.into_iter().rev().map(|(id, graph)| ExtractFrame {
631 id,
632 graph,
633 expanded: false,
634 }));
635 }
636}
637
638pub fn layout_flowchart_v2(
639 semantic: &Value,
640 effective_config: &MermaidConfig,
641 measurer: &dyn TextMeasurer,
642 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
643) -> Result<FlowchartV2Layout> {
644 let timing_enabled = std::env::var("MERMAN_FLOWCHART_LAYOUT_TIMING")
645 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
646 .unwrap_or(false);
647 let total_start = timing_enabled.then(std::time::Instant::now);
648
649 let deserialize_start = timing_enabled.then(std::time::Instant::now);
650 let model: FlowchartV2Model = crate::json::from_value_ref(semantic)?;
651 let deserialize = deserialize_start.map(|s| s.elapsed()).unwrap_or_default();
652
653 layout_flowchart_v2_with_model(
654 &model,
655 effective_config,
656 measurer,
657 math_renderer,
658 timing_enabled,
659 total_start,
660 deserialize,
661 )
662}
663
664pub fn layout_flowchart_v2_typed(
665 model: &FlowchartV2Model,
666 effective_config: &MermaidConfig,
667 measurer: &dyn TextMeasurer,
668 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
669) -> Result<FlowchartV2Layout> {
670 let timing_enabled = std::env::var("MERMAN_FLOWCHART_LAYOUT_TIMING")
671 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
672 .unwrap_or(false);
673 let total_start = timing_enabled.then(std::time::Instant::now);
674
675 layout_flowchart_v2_with_model(
676 model,
677 effective_config,
678 measurer,
679 math_renderer,
680 timing_enabled,
681 total_start,
682 std::time::Duration::default(),
683 )
684}
685
686fn layout_flowchart_v2_with_model(
687 model: &FlowchartV2Model,
688 effective_config: &MermaidConfig,
689 measurer: &dyn TextMeasurer,
690 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
691 timing_enabled: bool,
692 total_start: Option<std::time::Instant>,
693 deserialize: std::time::Duration,
694) -> Result<FlowchartV2Layout> {
695 #[derive(Debug, Default, Clone)]
696 struct FlowchartLayoutTimings {
697 total: std::time::Duration,
698 deserialize: std::time::Duration,
699 expand_self_loops: std::time::Duration,
700 build_graph: std::time::Duration,
701 extract_clusters: std::time::Duration,
702 dom_order: std::time::Duration,
703 layout_recursive: std::time::Duration,
704 dagre_calls: u32,
705 dagre_total: std::time::Duration,
706 place_graph: std::time::Duration,
707 build_output: std::time::Duration,
708 }
709
710 let mut timings = FlowchartLayoutTimings {
711 deserialize,
712 ..Default::default()
713 };
714
715 let effective_config_value = effective_config.as_value();
716
717 let expand_self_loops_start = timing_enabled.then(std::time::Instant::now);
721 let self_loop_count = model.edges.iter().filter(|e| e.from == e.to).count();
722 let mut render_edges: Vec<std::borrow::Cow<'_, FlowEdge>> =
723 Vec::with_capacity(model.edges.len() + self_loop_count * 3);
724 let mut self_loop_label_node_ids: Vec<String> = Vec::new();
725 let mut self_loop_label_node_id_set: std::collections::HashSet<String> =
726 std::collections::HashSet::new();
727 for e in &model.edges {
728 if e.from != e.to {
729 render_edges.push(std::borrow::Cow::Borrowed(e));
730 continue;
731 }
732
733 let helper_edges = super::flowchart_self_loop_helper_edges(
734 e,
735 super::FlowchartSelfLoopEdgeOptions::layout(),
736 );
737 if self_loop_label_node_id_set.insert(helper_edges.special_id_1.clone()) {
738 self_loop_label_node_ids.push(helper_edges.special_id_1.clone());
739 }
740 if self_loop_label_node_id_set.insert(helper_edges.special_id_2.clone()) {
741 self_loop_label_node_ids.push(helper_edges.special_id_2.clone());
742 }
743
744 render_edges.push(std::borrow::Cow::Owned(helper_edges.edge1));
747 render_edges.push(std::borrow::Cow::Owned(helper_edges.edge_mid));
748 render_edges.push(std::borrow::Cow::Owned(helper_edges.edge2));
749 }
750 if let Some(s) = expand_self_loops_start {
751 timings.expand_self_loops = s.elapsed();
752 }
753
754 let build_graph_start = timing_enabled.then(std::time::Instant::now);
755
756 let nodesep = config_f64(effective_config_value, &["flowchart", "nodeSpacing"]).unwrap_or(50.0);
757 let ranksep = config_f64(effective_config_value, &["flowchart", "rankSpacing"]).unwrap_or(50.0);
758 let node_padding =
760 config_f64(effective_config_value, &["flowchart", "padding"]).unwrap_or(15.0);
761 let state_padding = config_f64(effective_config_value, &["state", "padding"]).unwrap_or(8.0);
764 let wrapping_width =
765 config_f64(effective_config_value, &["flowchart", "wrappingWidth"]).unwrap_or(200.0);
766 let edge_label_wrapping_width = 200.0;
769 let cluster_title_wrapping_width = 200.0;
773 let node_html_labels = flowchart_effective_node_html_labels(effective_config_value);
777 let flowchart_html_labels = flowchart_effective_html_labels(effective_config_value);
778 let edge_html_labels = flowchart_html_labels;
779 let cluster_html_labels = edge_html_labels;
780 let node_html_label_css_parity = node_html_labels && flowchart_html_labels;
781 let node_wrap_mode = if node_html_labels {
782 WrapMode::HtmlLike
783 } else {
784 WrapMode::SvgLike
785 };
786 let cluster_wrap_mode = if cluster_html_labels {
787 WrapMode::HtmlLike
788 } else {
789 WrapMode::SvgLike
790 };
791 let edge_wrap_mode = if edge_html_labels {
792 WrapMode::HtmlLike
793 } else {
794 WrapMode::SvgLike
795 };
796 let cluster_padding = 8.0;
799 let title_margin_top = config_f64(
800 effective_config_value,
801 &["flowchart", "subGraphTitleMargin", "top"],
802 )
803 .unwrap_or(0.0);
804 let title_margin_bottom = config_f64(
805 effective_config_value,
806 &["flowchart", "subGraphTitleMargin", "bottom"],
807 )
808 .unwrap_or(0.0);
809 let title_total_margin = title_margin_top + title_margin_bottom;
810 let y_shift = title_total_margin / 2.0;
811 let inherit_dir = effective_config_value
812 .get("flowchart")
813 .and_then(|v| v.get("inheritDir"))
814 .and_then(Value::as_bool)
815 .unwrap_or(false);
816
817 fn normalize_css_font_family(font_family: &str) -> String {
818 let s = font_family.trim().trim_end_matches(';').trim();
819 if s.is_empty() {
820 return String::new();
821 }
822
823 let mut parts: Vec<String> = Vec::new();
824 let mut cur = String::new();
825 let mut in_single = false;
826 let mut in_double = false;
827
828 for ch in s.chars() {
829 match ch {
830 '\'' if !in_double => {
831 in_single = !in_single;
832 cur.push(ch);
833 }
834 '"' if !in_single => {
835 in_double = !in_double;
836 cur.push(ch);
837 }
838 ',' if !in_single && !in_double => {
839 let p = cur.trim();
840 if !p.is_empty() {
841 parts.push(p.to_string());
842 }
843 cur.clear();
844 }
845 _ => cur.push(ch),
846 }
847 }
848
849 let p = cur.trim();
850 if !p.is_empty() {
851 parts.push(p.to_string());
852 }
853 parts.join(",")
854 }
855
856 fn parse_font_size_px(v: &serde_json::Value) -> Option<f64> {
857 if let Some(n) = v.as_f64() {
858 return Some(n);
859 }
860 if let Some(n) = v.as_i64() {
861 return Some(n as f64);
862 }
863 if let Some(n) = v.as_u64() {
864 return Some(n as f64);
865 }
866 let s = v.as_str()?.trim();
867 if s.is_empty() {
868 return None;
869 }
870 let mut num = String::new();
871 for (idx, ch) in s.chars().enumerate() {
872 if ch.is_ascii_digit() {
873 num.push(ch);
874 continue;
875 }
876 if idx == 0 && (ch == '-' || ch == '+') {
877 num.push(ch);
878 continue;
879 }
880 break;
881 }
882 if num.trim().is_empty() {
883 return None;
884 }
885 num.parse::<f64>().ok()
886 }
887
888 let default_theme_font_family = "\"trebuchet ms\",verdana,arial,sans-serif".to_string();
889 let theme_font_family =
890 config_string(effective_config_value, &["themeVariables", "fontFamily"])
891 .map(|s| normalize_css_font_family(&s));
892 let top_font_family = config_string(effective_config_value, &["fontFamily"])
893 .map(|s| normalize_css_font_family(&s));
894 let font_family = Some(match (top_font_family, theme_font_family) {
895 (Some(top), Some(theme)) if theme == default_theme_font_family => top,
896 (_, Some(theme)) => theme,
897 (Some(top), None) => top,
898 (None, None) => default_theme_font_family,
899 });
900 let font_size = effective_config_value
901 .get("themeVariables")
902 .and_then(|tv| tv.get("fontSize"))
903 .and_then(parse_font_size_px)
904 .unwrap_or(16.0);
905 let font_weight = config_string(effective_config_value, &["fontWeight"]);
906 let text_style = TextStyle {
907 font_family,
908 font_size,
909 font_weight,
910 };
911 let html_label_text_style =
912 flowchart_html_label_measurement_base_style(&text_style, effective_config_value);
913 let node_label_base_style = if node_wrap_mode == WrapMode::HtmlLike {
914 &html_label_text_style
915 } else {
916 &text_style
917 };
918 let cluster_label_base_style = if cluster_wrap_mode == WrapMode::HtmlLike {
919 &html_label_text_style
920 } else {
921 &text_style
922 };
923 let edge_label_base_style = if edge_wrap_mode == WrapMode::HtmlLike {
924 &html_label_text_style
925 } else {
926 &text_style
927 };
928
929 let diagram_direction = normalize_dir(model.direction.as_deref().unwrap_or("TB"));
930 let has_subgraphs = !model.subgraphs.is_empty();
931 let mut subgraphs_by_id: std::collections::HashMap<String, FlowSubgraph> =
932 std::collections::HashMap::new();
933 for sg in &model.subgraphs {
934 subgraphs_by_id.insert(sg.id.clone(), sg.clone());
935 }
936 let subgraph_ids: std::collections::HashSet<&str> =
937 model.subgraphs.iter().map(|sg| sg.id.as_str()).collect();
938 let subgraph_id_set: std::collections::HashSet<String> =
939 model.subgraphs.iter().map(|sg| sg.id.clone()).collect();
940 let mut g: Graph<NodeLabel, EdgeLabel, GraphLabel> = Graph::new(GraphOptions {
941 multigraph: true,
942 compound: true,
945 directed: true,
946 });
947 g.set_graph(GraphLabel {
948 rankdir: rank_dir_from_flow(&diagram_direction),
949 nodesep,
950 ranksep,
951 marginx: 8.0,
952 marginy: 8.0,
953 acyclicer: None,
954 ..Default::default()
955 });
956
957 let mut empty_subgraph_ids: Vec<String> = Vec::new();
958 let mut cluster_node_labels: std::collections::HashMap<String, NodeLabel> =
959 std::collections::HashMap::new();
960 for sg in &model.subgraphs {
961 if sg.nodes.is_empty() {
962 empty_subgraph_ids.push(sg.id.clone());
965 continue;
966 }
967 cluster_node_labels.insert(sg.id.clone(), NodeLabel::default());
971 }
972
973 let mut leaf_node_labels: std::collections::HashMap<String, NodeLabel> =
974 std::collections::HashMap::new();
975 let mut leaf_label_metrics_by_id: HashMap<String, (f64, f64)> = HashMap::new();
976 leaf_label_metrics_by_id.reserve(model.nodes.len() + empty_subgraph_ids.len());
977 for n in &model.nodes {
978 if subgraph_ids.contains(n.id.as_str()) {
982 continue;
983 }
984 let raw_label = n.label.as_deref().unwrap_or(&n.id);
985 let label_type = n.label_type.as_deref().unwrap_or("text");
986 let node_text_style = flowchart_effective_text_style_for_node_classes(
987 node_label_base_style,
988 &model.class_defs,
989 &n.classes,
990 &n.styles,
991 );
992 let mut metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
993 measurer,
994 raw_label,
995 label_type,
996 style: node_text_style.as_ref(),
997 max_width_px: Some(wrapping_width),
998 wrap_mode: node_wrap_mode,
999 config: effective_config,
1000 math_renderer,
1001 preserve_string_whitespace_height: node_html_label_css_parity,
1002 });
1003 let span_css_height_parity =
1004 flowchart_node_has_span_css_height_parity(&model.class_defs, &n.classes);
1005 if node_html_label_css_parity && span_css_height_parity {
1006 crate::text::flowchart_apply_mermaid_styled_node_height_parity(
1007 &mut metrics,
1008 node_text_style.as_ref(),
1009 );
1010 }
1011 if node_wrap_mode == WrapMode::SvgLike
1012 && label_type != "markdown"
1013 && !raw_label.contains('<')
1014 && !raw_label.contains('>')
1015 && matches!(
1016 n.layout_shape.as_deref().unwrap_or("squareRect"),
1017 "squareRect"
1018 )
1019 {
1020 let plain = crate::flowchart::flowchart_label_plain_text_for_layout(
1021 raw_label, label_type, false,
1022 );
1023 metrics.width = flowchart_svg_plain_computed_width_px(
1024 measurer,
1025 &plain,
1026 node_text_style.as_ref(),
1027 Some(wrapping_width),
1028 );
1029 }
1030 leaf_label_metrics_by_id.insert(n.id.clone(), (metrics.width, metrics.height));
1031 let (width, height) = node_layout_dimensions(NodeLayoutDimensionsRequest {
1032 layout_shape: n.layout_shape.as_deref(),
1033 layout_direction: &diagram_direction,
1034 metrics,
1035 padding: node_padding,
1036 state_padding,
1037 wrap_mode: node_wrap_mode,
1038 node_icon: n.icon.as_deref(),
1039 node_img: n.img.as_deref(),
1040 node_pos: n.pos.as_deref(),
1041 node_asset_width: n.asset_width,
1042 node_asset_height: n.asset_height,
1043 });
1044 leaf_node_labels.insert(
1045 n.id.clone(),
1046 NodeLabel {
1047 width,
1048 height,
1049 ..Default::default()
1050 },
1051 );
1052 }
1053 for sg in &model.subgraphs {
1054 if !sg.nodes.is_empty() {
1055 continue;
1056 }
1057 let label_type = sg.label_type.as_deref().unwrap_or("text");
1058 let sg_text_style = flowchart_effective_text_style_for_classes(
1059 cluster_label_base_style,
1060 &model.class_defs,
1061 &sg.classes,
1062 &[],
1063 );
1064 let metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
1065 measurer,
1066 raw_label: &sg.title,
1067 label_type,
1068 style: sg_text_style.as_ref(),
1069 max_width_px: Some(cluster_title_wrapping_width),
1070 wrap_mode: node_wrap_mode,
1071 config: effective_config,
1072 math_renderer,
1073 preserve_string_whitespace_height: node_html_label_css_parity,
1074 });
1075 leaf_label_metrics_by_id.insert(sg.id.clone(), (metrics.width, metrics.height));
1076 let (width, height) = node_layout_dimensions(NodeLayoutDimensionsRequest {
1077 layout_shape: Some("squareRect"),
1078 layout_direction: &diagram_direction,
1079 metrics,
1080 padding: cluster_padding,
1081 state_padding,
1082 wrap_mode: node_wrap_mode,
1083 node_icon: None,
1084 node_img: None,
1085 node_pos: None,
1086 node_asset_width: None,
1087 node_asset_height: None,
1088 });
1089 leaf_node_labels.insert(
1090 sg.id.clone(),
1091 NodeLabel {
1092 width,
1093 height,
1094 ..Default::default()
1095 },
1096 );
1097 }
1098
1099 let mut inserted: std::collections::HashSet<String> = std::collections::HashSet::new();
1107 let mut parent_assigned: std::collections::HashSet<String> = std::collections::HashSet::new();
1108 let insert_one = |id: &str,
1109 g: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1110 inserted: &mut std::collections::HashSet<String>| {
1111 if inserted.contains(id) {
1112 return;
1113 }
1114 if let Some(lbl) = cluster_node_labels.get(id).cloned() {
1115 g.set_node(id.to_string(), lbl);
1116 inserted.insert(id.to_string());
1117 return;
1118 }
1119 if let Some(lbl) = leaf_node_labels.get(id).cloned() {
1120 g.set_node(id.to_string(), lbl);
1121 inserted.insert(id.to_string());
1122 }
1123 };
1124
1125 if has_subgraphs {
1126 let mut parent_by_id: std::collections::HashMap<String, String> =
1129 std::collections::HashMap::new();
1130 for sg in model.subgraphs.iter().rev() {
1131 for child in &sg.nodes {
1132 parent_by_id.insert(child.clone(), sg.id.clone());
1133 }
1134 }
1135
1136 let insert_with_parent =
1137 |id: &str,
1138 g: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1139 inserted: &mut std::collections::HashSet<String>,
1140 parent_assigned: &mut std::collections::HashSet<String>| {
1141 insert_one(id, g, inserted);
1142 if !parent_assigned.insert(id.to_string()) {
1143 return;
1144 }
1145 if let Some(p) = parent_by_id.get(id).cloned() {
1146 g.set_parent(id.to_string(), p);
1147 }
1148 };
1149
1150 for sg in model.subgraphs.iter().rev() {
1151 insert_with_parent(sg.id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1152 }
1153 for n in &model.nodes {
1154 insert_with_parent(n.id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1155 }
1156 for id in &model.vertex_calls {
1157 insert_with_parent(id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1158 }
1159 } else {
1160 for n in &model.nodes {
1162 insert_one(n.id.as_str(), &mut g, &mut inserted);
1163 }
1164 for id in &model.vertex_calls {
1165 insert_one(id.as_str(), &mut g, &mut inserted);
1166 }
1167 }
1168
1169 for id in &self_loop_label_node_ids {
1172 if !g.has_node(id) {
1173 g.set_node(
1174 id.clone(),
1175 NodeLabel {
1176 width: 0.1_f32 as f64,
1181 height: 0.1_f32 as f64,
1182 ..Default::default()
1183 },
1184 );
1185 }
1186 let Some((base, _)) = id.split_once("---") else {
1187 continue;
1188 };
1189 if let Some(p) = g.parent(base) {
1190 g.set_parent(id.clone(), p.to_string());
1191 }
1192 }
1193
1194 let effective_dir_by_id = if has_subgraphs {
1195 compute_effective_dir_by_id(&subgraphs_by_id, &g, &diagram_direction, inherit_dir)
1196 } else {
1197 HashMap::new()
1198 };
1199
1200 let mut edge_key_by_id: HashMap<String, String> = HashMap::new();
1204 let mut edge_id_by_key: HashMap<String, String> = HashMap::new();
1205
1206 for e in &render_edges {
1207 let edge_key = if let Some(prefix) = e.id.strip_suffix("-cyclic-special-1") {
1210 format!("{prefix}-cyclic-special-0")
1211 } else if let Some(prefix) = e.id.strip_suffix("-cyclic-special-mid") {
1212 format!("{prefix}-cyclic-special-1")
1213 } else if let Some(prefix) = e.id.strip_suffix("-cyclic-special-2") {
1214 format!("{prefix}-cyc<lic-special-2")
1217 } else {
1218 e.id.clone()
1219 };
1220 edge_key_by_id.insert(e.id.clone(), edge_key.clone());
1221 edge_id_by_key.insert(edge_key.clone(), e.id.clone());
1222
1223 let from = e.from.clone();
1224 let to = e.to.clone();
1225
1226 if edge_label_is_non_empty(e) {
1227 let label_text = e.label.as_deref().unwrap_or_default();
1228 let label_type = e.label_type.as_deref().unwrap_or("text");
1229 let edge_text_style = flowchart_effective_text_style_for_classes(
1230 edge_label_base_style,
1231 &model.class_defs,
1232 &e.classes,
1233 &e.style,
1234 );
1235 let metrics = if label_type == "markdown" && edge_wrap_mode != WrapMode::HtmlLike {
1236 crate::text::measure_wrapped_markdown_with_flowchart_bold_deltas(
1237 measurer,
1238 label_text,
1239 edge_text_style.as_ref(),
1240 Some(edge_label_wrapping_width),
1241 edge_wrap_mode,
1242 )
1243 } else {
1244 flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
1245 measurer,
1246 raw_label: label_text,
1247 label_type,
1248 style: edge_text_style.as_ref(),
1249 max_width_px: Some(edge_label_wrapping_width),
1250 wrap_mode: edge_wrap_mode,
1251 config: effective_config,
1252 math_renderer,
1253 preserve_string_whitespace_height: false,
1254 })
1255 };
1256 let (label_width, label_height) = if edge_html_labels {
1257 (metrics.width.max(1.0), metrics.height.max(1.0))
1258 } else {
1259 (
1262 (metrics.width + 4.0).max(1.0),
1263 (metrics.height + 4.0).max(1.0),
1264 )
1265 };
1266
1267 let minlen = e.length.max(1);
1268 let el = EdgeLabel {
1269 width: label_width,
1270 height: label_height,
1271 labelpos: LabelPos::C,
1272 labeloffset: 10.0,
1274 minlen,
1275 weight: 1.0,
1276 ..Default::default()
1277 };
1278
1279 g.set_edge_named(from, to, Some(edge_key), Some(el));
1280 } else {
1281 let el = EdgeLabel {
1282 width: 0.0,
1283 height: 0.0,
1284 labelpos: LabelPos::C,
1285 labeloffset: 10.0,
1287 minlen: e.length.max(1),
1288 weight: 1.0,
1289 ..Default::default()
1290 };
1291 g.set_edge_named(from, to, Some(edge_key), Some(el));
1292 }
1293 }
1294
1295 let external_connections_by_id = if has_subgraphs {
1296 adjust_flowchart_clusters_and_edges(&mut g)
1297 } else {
1298 std::collections::HashMap::new()
1299 };
1300
1301 let mut edge_endpoints_by_id: HashMap<String, (String, String)> = HashMap::new();
1302 for ek in g.edge_keys() {
1303 let Some(edge_key) = ek.name.as_deref() else {
1304 continue;
1305 };
1306 let edge_id = edge_id_by_key
1307 .get(edge_key)
1308 .cloned()
1309 .unwrap_or_else(|| edge_key.to_string());
1310 edge_endpoints_by_id.insert(edge_id, (ek.v.clone(), ek.w.clone()));
1311 }
1312
1313 if let Some(s) = build_graph_start {
1314 timings.build_graph = s.elapsed();
1315 }
1316
1317 let mut extracted_graphs: std::collections::HashMap<
1318 String,
1319 Graph<NodeLabel, EdgeLabel, GraphLabel>,
1320 > = std::collections::HashMap::new();
1321 if has_subgraphs {
1322 let extract_start = timing_enabled.then(std::time::Instant::now);
1323 extract_clusters_recursively(
1324 &mut g,
1325 &subgraphs_by_id,
1326 &external_connections_by_id,
1327 &mut extracted_graphs,
1328 0,
1329 );
1330 if let Some(s) = extract_start {
1331 timings.extract_clusters = s.elapsed();
1332 }
1333 }
1334
1335 let mut dom_node_order_by_root: std::collections::HashMap<String, Vec<String>> =
1339 std::collections::HashMap::new();
1340 let dom_order_start = timing_enabled.then(std::time::Instant::now);
1341 dom_node_order_by_root.insert(String::new(), g.node_ids());
1342 for (id, cg) in &extracted_graphs {
1343 dom_node_order_by_root.insert(id.clone(), cg.node_ids());
1344 }
1345 if let Some(s) = dom_order_start {
1346 timings.dom_order = s.elapsed();
1347 }
1348
1349 type Rect = merman_core::geom::Box2;
1350
1351 struct ClusterTitleMetricsContext<'a> {
1352 subgraphs_by_id: &'a std::collections::HashMap<String, FlowSubgraph>,
1353 measurer: &'a dyn TextMeasurer,
1354 text_style: &'a TextStyle,
1355 html_label_text_style: &'a TextStyle,
1356 title_wrapping_width: f64,
1357 wrap_mode: WrapMode,
1358 config: &'a MermaidConfig,
1359 math_renderer: Option<&'a (dyn MathRenderer + Send + Sync)>,
1360 }
1361
1362 fn cluster_title_metrics_for_layout(
1363 id: &str,
1364 ctx: &ClusterTitleMetricsContext<'_>,
1365 ) -> Option<(f64, f64)> {
1366 let sg = ctx.subgraphs_by_id.get(id)?;
1367 let label_type = sg.label_type.as_deref().unwrap_or("text");
1368 let metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
1369 measurer: ctx.measurer,
1370 raw_label: &sg.title,
1371 label_type,
1372 style: if ctx.wrap_mode == WrapMode::HtmlLike {
1373 ctx.html_label_text_style
1374 } else {
1375 ctx.text_style
1376 },
1377 max_width_px: Some(ctx.title_wrapping_width),
1378 wrap_mode: ctx.wrap_mode,
1379 config: ctx.config,
1380 math_renderer: ctx.math_renderer,
1381 preserve_string_whitespace_height: false,
1382 });
1383 Some((metrics.width.max(1.0), metrics.height.max(1.0)))
1384 }
1385
1386 fn extracted_graph_bbox_rect(
1387 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1388 title_total_margin: f64,
1389 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1390 subgraph_id_set: &std::collections::HashSet<String>,
1391 title_metrics_ctx: &ClusterTitleMetricsContext<'_>,
1392 cluster_padding: f64,
1393 ) -> Option<Rect> {
1394 fn graph_content_rect(
1395 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1396 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1397 subgraph_id_set: &std::collections::HashSet<String>,
1398 title_total_margin: f64,
1399 title_metrics_ctx: &ClusterTitleMetricsContext<'_>,
1400 cluster_padding: f64,
1401 ) -> Option<Rect> {
1402 let mut out: Option<Rect> = None;
1403 for id in g.node_ids() {
1404 let Some(n) = g.node(&id) else { continue };
1405 let (Some(x), Some(y)) = (n.x, n.y) else {
1406 continue;
1407 };
1408 let mut width = n.width;
1409 let mut height = n.height;
1410 let is_cluster_node = extracted.contains_key(&id) && g.children(&id).is_empty();
1411 let is_non_recursive_cluster =
1412 subgraph_id_set.contains(&id) && !g.children(&id).is_empty();
1413
1414 if !is_cluster_node && is_non_recursive_cluster {
1421 if title_total_margin > 0.0 {
1422 height = (height + title_total_margin).max(1.0);
1423 }
1424 if let Some((title_w, title_h)) =
1425 cluster_title_metrics_for_layout(&id, title_metrics_ctx)
1426 {
1427 width = width.max(title_w + cluster_padding);
1428 height = height.max(title_h + title_total_margin);
1429 }
1430 }
1431
1432 let r = Rect::from_center(x, y, width, height);
1433 if let Some(ref mut cur) = out {
1434 cur.union(r);
1435 } else {
1436 out = Some(r);
1437 }
1438 }
1439 for ek in g.edge_keys() {
1440 let Some(e) = g.edge_by_key(&ek) else {
1441 continue;
1442 };
1443 let (Some(x), Some(y)) = (e.x, e.y) else {
1444 continue;
1445 };
1446 if e.width <= 0.0 && e.height <= 0.0 {
1447 continue;
1448 }
1449 let r = Rect::from_center(x, y, e.width, e.height);
1450 if let Some(ref mut cur) = out {
1451 cur.union(r);
1452 } else {
1453 out = Some(r);
1454 }
1455 }
1456 out
1457 }
1458
1459 graph_content_rect(
1460 g,
1461 extracted,
1462 subgraph_id_set,
1463 title_total_margin,
1464 title_metrics_ctx,
1465 cluster_padding,
1466 )
1467 }
1468
1469 fn apply_mermaid_subgraph_title_shifts(
1470 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1471 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1472 subgraph_id_set: &std::collections::HashSet<String>,
1473 y_shift: f64,
1474 ) {
1475 if y_shift.abs() < 1e-9 {
1476 return;
1477 }
1478
1479 for id in graph.node_ids() {
1484 let is_cluster_node = extracted.contains_key(&id) && graph.children(&id).is_empty();
1488 let delta_y = if is_cluster_node {
1489 y_shift * 2.0
1490 } else if subgraph_id_set.contains(&id) && !graph.children(&id).is_empty() {
1491 0.0
1492 } else {
1493 y_shift
1494 };
1495 if delta_y.abs() > 1e-9 {
1496 let Some(y) = graph.node(&id).and_then(|n| n.y) else {
1497 continue;
1498 };
1499 if let Some(n) = graph.node_mut(&id) {
1500 n.y = Some(y + delta_y);
1501 }
1502 }
1503 }
1504
1505 for ek in graph.edge_keys() {
1507 let Some(e) = graph.edge_mut_by_key(&ek) else {
1508 continue;
1509 };
1510 if let Some(y) = e.y {
1511 e.y = Some(y + y_shift);
1512 }
1513 for p in &mut e.points {
1514 p.y += y_shift;
1515 }
1516 }
1517 }
1518
1519 struct RecursiveLayoutContext<'a> {
1520 extracted:
1521 &'a mut std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1522 subgraph_id_set: &'a std::collections::HashSet<String>,
1523 y_shift: f64,
1524 cluster_node_labels: &'a std::collections::HashMap<String, NodeLabel>,
1525 title_total_margin: f64,
1526 title_metrics_ctx: &'a ClusterTitleMetricsContext<'a>,
1527 cluster_padding: f64,
1528 timings: &'a mut FlowchartLayoutTimings,
1529 timing_enabled: bool,
1530 }
1531
1532 fn layout_graph_with_recursive_clusters(
1533 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1534 graph_cluster_id: Option<&str>,
1535 _depth: usize,
1536 ctx: &mut RecursiveLayoutContext<'_>,
1537 ) {
1538 #[derive(Clone)]
1539 struct LayoutFrame {
1540 cluster_id: Option<String>,
1541 expanded: bool,
1542 }
1543
1544 fn recursive_child_ids(
1545 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1546 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1547 ) -> Vec<String> {
1548 graph
1549 .node_ids()
1550 .into_iter()
1551 .filter(|id| graph.children(id).is_empty() && extracted.contains_key(id))
1555 .collect()
1556 }
1557
1558 fn inject_parent_cluster(
1559 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1560 cluster_id: &str,
1561 ctx: &RecursiveLayoutContext<'_>,
1562 ) {
1563 if !graph.has_node(cluster_id) {
1564 let lbl = ctx
1565 .cluster_node_labels
1566 .get(cluster_id)
1567 .cloned()
1568 .unwrap_or_default();
1569 graph.set_node(cluster_id.to_string(), lbl);
1570 }
1571 let node_ids = graph.node_ids();
1572 for node_id in node_ids {
1573 if node_id == cluster_id {
1574 continue;
1575 }
1576 if graph.parent(&node_id).is_none() {
1577 graph.set_parent(node_id, cluster_id.to_string());
1578 }
1579 }
1580 }
1581
1582 fn update_child_cluster_bounds(
1583 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1584 ctx: &RecursiveLayoutContext<'_>,
1585 ) {
1586 let ids = recursive_child_ids(graph, ctx.extracted);
1587 for id in ids {
1588 let Some(child) = ctx.extracted.get(&id) else {
1589 continue;
1590 };
1591
1592 if let Some(r) = extracted_graph_bbox_rect(
1597 child,
1598 ctx.title_total_margin,
1599 ctx.extracted,
1600 ctx.subgraph_id_set,
1601 ctx.title_metrics_ctx,
1602 ctx.cluster_padding,
1603 ) {
1604 if let Some(n) = graph.node_mut(&id) {
1605 n.width = r.width().max(1.0);
1606 n.height = r.height().max(1.0);
1607 }
1608 } else if let Some(n_child) = child.node(&id) {
1609 if let Some(n) = graph.node_mut(&id) {
1610 n.width = n_child.width.max(1.0);
1611 n.height = n_child.height.max(1.0);
1612 }
1613 }
1614 }
1615 }
1616
1617 fn layout_one_graph(
1618 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1619 ctx: &mut RecursiveLayoutContext<'_>,
1620 ) {
1621 if ctx.timing_enabled {
1622 ctx.timings.dagre_calls += 1;
1623 let start = std::time::Instant::now();
1624 dugong::layout_dagreish(graph);
1625 ctx.timings.dagre_total += start.elapsed();
1626 } else {
1627 dugong::layout_dagreish(graph);
1628 }
1629 apply_mermaid_subgraph_title_shifts(
1630 graph,
1631 ctx.extracted,
1632 ctx.subgraph_id_set,
1633 ctx.y_shift,
1634 );
1635 }
1636
1637 let mut stack = vec![LayoutFrame {
1638 cluster_id: graph_cluster_id.map(str::to_string),
1639 expanded: false,
1640 }];
1641
1642 while let Some(frame) = stack.pop() {
1643 if !frame.expanded {
1644 let Some((child_ids, parent_nodesep, parent_ranksep)) = (match &frame.cluster_id {
1645 Some(id) => ctx.extracted.get(id).map(|g| {
1646 (
1647 recursive_child_ids(g, ctx.extracted),
1648 g.graph().nodesep,
1649 g.graph().ranksep,
1650 )
1651 }),
1652 None => Some((
1653 recursive_child_ids(graph, ctx.extracted),
1654 graph.graph().nodesep,
1655 graph.graph().ranksep,
1656 )),
1657 }) else {
1658 continue;
1659 };
1660
1661 stack.push(LayoutFrame {
1662 cluster_id: frame.cluster_id,
1663 expanded: true,
1664 });
1665
1666 for child_id in child_ids.iter().rev() {
1667 if let Some(child) = ctx.extracted.get_mut(child_id) {
1672 child.graph_mut().nodesep = parent_nodesep;
1673 child.graph_mut().ranksep = parent_ranksep + 25.0;
1674 }
1675 stack.push(LayoutFrame {
1676 cluster_id: Some(child_id.clone()),
1677 expanded: false,
1678 });
1679 }
1680 continue;
1681 }
1682
1683 if let Some(cluster_id) = frame.cluster_id {
1684 let Some(mut current) = ctx.extracted.remove(&cluster_id) else {
1685 continue;
1686 };
1687 update_child_cluster_bounds(&mut current, ctx);
1688 inject_parent_cluster(&mut current, &cluster_id, ctx);
1689 layout_one_graph(&mut current, ctx);
1690 ctx.extracted.insert(cluster_id, current);
1691 } else {
1692 update_child_cluster_bounds(graph, ctx);
1693 layout_one_graph(graph, ctx);
1694 }
1695 }
1696 }
1697
1698 let layout_start = timing_enabled.then(std::time::Instant::now);
1699 {
1700 let title_metrics_ctx = ClusterTitleMetricsContext {
1701 subgraphs_by_id: &subgraphs_by_id,
1702 measurer,
1703 text_style: &text_style,
1704 html_label_text_style: &html_label_text_style,
1705 title_wrapping_width: cluster_title_wrapping_width,
1706 wrap_mode: cluster_wrap_mode,
1707 config: effective_config,
1708 math_renderer,
1709 };
1710 let mut recursive_layout_ctx = RecursiveLayoutContext {
1711 extracted: &mut extracted_graphs,
1712 subgraph_id_set: &subgraph_id_set,
1713 y_shift,
1714 cluster_node_labels: &cluster_node_labels,
1715 title_total_margin,
1716 title_metrics_ctx: &title_metrics_ctx,
1717 cluster_padding,
1718 timings: &mut timings,
1719 timing_enabled,
1720 };
1721 layout_graph_with_recursive_clusters(&mut g, None, 0, &mut recursive_layout_ctx);
1722 }
1723 if let Some(s) = layout_start {
1724 timings.layout_recursive = s.elapsed();
1725 }
1726
1727 let mut leaf_rects: std::collections::HashMap<String, Rect> = std::collections::HashMap::new();
1728 let mut base_pos: std::collections::HashMap<String, (f64, f64)> =
1729 std::collections::HashMap::new();
1730 let mut edge_override_points: std::collections::HashMap<String, Vec<LayoutPoint>> =
1731 std::collections::HashMap::new();
1732 let mut edge_override_label: std::collections::HashMap<String, Option<LayoutLabel>> =
1733 std::collections::HashMap::new();
1734 let mut edge_override_from_cluster: std::collections::HashMap<String, Option<String>> =
1735 std::collections::HashMap::new();
1736 let mut edge_override_to_cluster: std::collections::HashMap<String, Option<String>> =
1737 std::collections::HashMap::new();
1738 let mut leaf_node_ids: std::collections::HashSet<String> = model
1739 .nodes
1740 .iter()
1741 .filter(|n| !subgraph_ids.contains(n.id.as_str()))
1742 .map(|n| n.id.clone())
1743 .collect();
1744 for id in &self_loop_label_node_ids {
1745 leaf_node_ids.insert(id.clone());
1746 }
1747 for id in &empty_subgraph_ids {
1748 leaf_node_ids.insert(id.clone());
1749 }
1750
1751 struct PlaceGraphInputs<'a> {
1752 edge_id_by_key: &'a std::collections::HashMap<String, String>,
1753 extracted_graphs:
1754 &'a std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1755 subgraph_ids: &'a std::collections::HashSet<&'a str>,
1756 leaf_node_ids: &'a std::collections::HashSet<String>,
1757 }
1758
1759 struct PlaceGraphOutputs<'a> {
1760 base_pos: &'a mut std::collections::HashMap<String, (f64, f64)>,
1761 leaf_rects: &'a mut std::collections::HashMap<String, Rect>,
1762 cluster_rects_from_graph: &'a mut std::collections::HashMap<String, Rect>,
1763 extracted_cluster_rects: &'a mut std::collections::HashMap<String, Rect>,
1764 extracted_cluster_base_widths: &'a mut std::collections::HashMap<String, f64>,
1765 edge_override_points: &'a mut std::collections::HashMap<String, Vec<LayoutPoint>>,
1766 edge_override_label: &'a mut std::collections::HashMap<String, Option<LayoutLabel>>,
1767 edge_override_from_cluster: &'a mut std::collections::HashMap<String, Option<String>>,
1768 edge_override_to_cluster: &'a mut std::collections::HashMap<String, Option<String>>,
1769 }
1770
1771 fn place_graph(
1772 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1773 offset: (f64, f64),
1774 is_root: bool,
1775 inputs: &PlaceGraphInputs<'_>,
1776 out: &mut PlaceGraphOutputs<'_>,
1777 ) {
1778 for id in graph.node_ids() {
1779 let Some(n) = graph.node(&id) else { continue };
1780 let x = n.x.unwrap_or(0.0) + offset.0;
1781 let y = n.y.unwrap_or(0.0) + offset.1;
1782 if inputs.leaf_node_ids.contains(&id) {
1783 out.base_pos.insert(id.clone(), (x, y));
1784 out.leaf_rects
1785 .insert(id.clone(), Rect::from_center(x, y, n.width, n.height));
1786 continue;
1787 }
1788 }
1789
1790 fn subtree_rect(
1791 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1792 id: &str,
1793 visiting: &mut std::collections::HashSet<String>,
1794 ) -> Option<Rect> {
1795 if !visiting.insert(id.to_string()) {
1796 return None;
1797 }
1798 let mut out: Option<Rect> = None;
1799 for child in graph.children(id) {
1800 if let Some(n) = graph.node(child) {
1801 if let (Some(x), Some(y)) = (n.x, n.y) {
1802 let r = Rect::from_center(x, y, n.width, n.height);
1803 if let Some(ref mut cur) = out {
1804 cur.union(r);
1805 } else {
1806 out = Some(r);
1807 }
1808 }
1809 }
1810 if !graph.children(child).is_empty() {
1811 if let Some(r) = subtree_rect(graph, child, visiting) {
1812 if let Some(ref mut cur) = out {
1813 cur.union(r);
1814 } else {
1815 out = Some(r);
1816 }
1817 }
1818 }
1819 }
1820 visiting.remove(id);
1821 out
1822 }
1823
1824 for id in graph.node_ids() {
1830 if !inputs.subgraph_ids.contains(id.as_str()) {
1831 continue;
1832 }
1833 if inputs.extracted_graphs.contains_key(&id) {
1834 continue;
1835 }
1836 if out.cluster_rects_from_graph.contains_key(&id) {
1837 continue;
1838 }
1839 if let Some(n) = graph.node(&id) {
1840 if let (Some(x), Some(y)) = (n.x, n.y) {
1841 if n.width > 0.0 && n.height > 0.0 {
1842 let mut r = Rect::from_center(x, y, n.width, n.height);
1843 r.translate(offset.0, offset.1);
1844 out.cluster_rects_from_graph.insert(id, r);
1845 continue;
1846 }
1847 }
1848 }
1849
1850 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
1851 let Some(mut r) = subtree_rect(graph, &id, &mut visiting) else {
1852 continue;
1853 };
1854 r.translate(offset.0, offset.1);
1855 out.cluster_rects_from_graph.insert(id, r);
1856 }
1857
1858 for ek in graph.edge_keys() {
1859 let Some(edge_key) = ek.name.as_deref() else {
1860 continue;
1861 };
1862 let edge_id = inputs
1863 .edge_id_by_key
1864 .get(edge_key)
1865 .map(String::as_str)
1866 .unwrap_or(edge_key);
1867 let Some(lbl) = graph.edge_by_key(&ek) else {
1868 continue;
1869 };
1870
1871 if let (Some(x), Some(y)) = (lbl.x, lbl.y) {
1872 if lbl.width > 0.0 || lbl.height > 0.0 {
1873 let lx = x + offset.0;
1874 let ly = y + offset.1;
1875 let leaf_id = format!("edge-label::{edge_id}");
1876 out.base_pos.insert(leaf_id.clone(), (lx, ly));
1877 out.leaf_rects
1878 .insert(leaf_id, Rect::from_center(lx, ly, lbl.width, lbl.height));
1879 }
1880 }
1881
1882 if !is_root {
1883 let points = lbl
1884 .points
1885 .iter()
1886 .map(|p| LayoutPoint {
1887 x: p.x + offset.0,
1888 y: p.y + offset.1,
1889 })
1890 .collect::<Vec<_>>();
1891 let label_pos = match (lbl.x, lbl.y) {
1892 (Some(x), Some(y)) if lbl.width > 0.0 || lbl.height > 0.0 => {
1893 Some(LayoutLabel {
1894 x: x + offset.0,
1895 y: y + offset.1,
1896 width: lbl.width,
1897 height: lbl.height,
1898 })
1899 }
1900 _ => None,
1901 };
1902 out.edge_override_points.insert(edge_id.to_string(), points);
1903 out.edge_override_label
1904 .insert(edge_id.to_string(), label_pos);
1905 let from_cluster = lbl
1906 .extras
1907 .get("fromCluster")
1908 .and_then(|v| v.as_str().map(|s| s.to_string()));
1909 let to_cluster = lbl
1910 .extras
1911 .get("toCluster")
1912 .and_then(|v| v.as_str().map(|s| s.to_string()));
1913 out.edge_override_from_cluster
1914 .insert(edge_id.to_string(), from_cluster);
1915 out.edge_override_to_cluster
1916 .insert(edge_id.to_string(), to_cluster);
1917 }
1918 }
1919
1920 for id in graph.node_ids() {
1921 if !graph.children(&id).is_empty() {
1925 continue;
1926 }
1927 let Some(child) = inputs.extracted_graphs.get(&id) else {
1928 continue;
1929 };
1930 let Some(n) = graph.node(&id) else {
1931 continue;
1932 };
1933 let (Some(px), Some(py)) = (n.x, n.y) else {
1934 continue;
1935 };
1936 let parent_x = px + offset.0;
1937 let parent_y = py + offset.1;
1938 let Some(cnode) = child.node(&id) else {
1939 continue;
1940 };
1941 let (Some(cx), Some(cy)) = (cnode.x, cnode.y) else {
1942 continue;
1943 };
1944 let child_offset = (parent_x - cx, parent_y - cy);
1945 let r = Rect::from_center(parent_x, parent_y, n.width, n.height);
1950 out.extracted_cluster_rects.insert(id.clone(), r);
1951 out.extracted_cluster_base_widths
1952 .insert(id.clone(), cnode.width.max(1.0));
1953 place_graph(child, child_offset, false, inputs, out);
1954 }
1955 }
1956
1957 let mut cluster_rects_from_graph: std::collections::HashMap<String, Rect> =
1958 std::collections::HashMap::new();
1959 let mut extracted_cluster_rects: std::collections::HashMap<String, Rect> =
1960 std::collections::HashMap::new();
1961 let mut extracted_cluster_base_widths: std::collections::HashMap<String, f64> =
1962 std::collections::HashMap::new();
1963 let place_start = timing_enabled.then(std::time::Instant::now);
1964 {
1965 let place_graph_inputs = PlaceGraphInputs {
1966 edge_id_by_key: &edge_id_by_key,
1967 extracted_graphs: &extracted_graphs,
1968 subgraph_ids: &subgraph_ids,
1969 leaf_node_ids: &leaf_node_ids,
1970 };
1971 let mut place_graph_outputs = PlaceGraphOutputs {
1972 base_pos: &mut base_pos,
1973 leaf_rects: &mut leaf_rects,
1974 cluster_rects_from_graph: &mut cluster_rects_from_graph,
1975 extracted_cluster_rects: &mut extracted_cluster_rects,
1976 extracted_cluster_base_widths: &mut extracted_cluster_base_widths,
1977 edge_override_points: &mut edge_override_points,
1978 edge_override_label: &mut edge_override_label,
1979 edge_override_from_cluster: &mut edge_override_from_cluster,
1980 edge_override_to_cluster: &mut edge_override_to_cluster,
1981 };
1982 place_graph(
1983 &g,
1984 (0.0, 0.0),
1985 true,
1986 &place_graph_inputs,
1987 &mut place_graph_outputs,
1988 );
1989 }
1990 if let Some(s) = place_start {
1991 timings.place_graph = s.elapsed();
1992 }
1993
1994 let build_output_start = timing_enabled.then(std::time::Instant::now);
1995
1996 let mut extra_children: std::collections::HashMap<String, Vec<String>> =
1997 std::collections::HashMap::new();
1998 let labeled_edges: std::collections::HashSet<&str> = render_edges
1999 .iter()
2000 .filter(|e| edge_label_is_non_empty(e))
2001 .map(|e| e.id.as_str())
2002 .collect();
2003
2004 fn collect_extra_children(
2005 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
2006 edge_id_by_key: &std::collections::HashMap<String, String>,
2007 labeled_edges: &std::collections::HashSet<&str>,
2008 implicit_root: Option<&str>,
2009 out: &mut std::collections::HashMap<String, Vec<String>>,
2010 ) {
2011 for ek in graph.edge_keys() {
2012 let Some(edge_key) = ek.name.as_deref() else {
2013 continue;
2014 };
2015 let edge_id = edge_id_by_key
2016 .get(edge_key)
2017 .map(String::as_str)
2018 .unwrap_or(edge_key);
2019 if !labeled_edges.contains(edge_id) {
2020 continue;
2021 }
2022 let parent = lowest_common_parent(graph, &ek.v, &ek.w)
2028 .or_else(|| implicit_root.map(|s| s.to_string()));
2029 let Some(parent) = parent else {
2030 continue;
2031 };
2032 out.entry(parent)
2033 .or_default()
2034 .push(format!("edge-label::{edge_id}"));
2035 }
2036 }
2037
2038 collect_extra_children(
2039 &g,
2040 &edge_id_by_key,
2041 &labeled_edges,
2042 None,
2043 &mut extra_children,
2044 );
2045 for (cluster_id, cg) in &extracted_graphs {
2046 collect_extra_children(
2047 cg,
2048 &edge_id_by_key,
2049 &labeled_edges,
2050 Some(cluster_id.as_str()),
2051 &mut extra_children,
2052 );
2053 }
2054
2055 for id in &self_loop_label_node_ids {
2059 if let Some(p) = g.parent(id) {
2060 extra_children
2061 .entry(p.to_string())
2062 .or_default()
2063 .push(id.clone());
2064 }
2065 }
2066
2067 let mut out_nodes: Vec<LayoutNode> = Vec::new();
2068 for n in &model.nodes {
2069 if subgraph_ids.contains(n.id.as_str()) {
2070 continue;
2071 }
2072 let (x, y) = base_pos
2073 .get(&n.id)
2074 .copied()
2075 .ok_or_else(|| Error::InvalidModel {
2076 message: format!("missing positioned node {}", n.id),
2077 })?;
2078 let (width, height) = leaf_rects
2079 .get(&n.id)
2080 .map(|r| (r.width(), r.height()))
2081 .ok_or_else(|| Error::InvalidModel {
2082 message: format!("missing sized node {}", n.id),
2083 })?;
2084 out_nodes.push(LayoutNode {
2085 id: n.id.clone(),
2086 x,
2087 y,
2088 width,
2089 height,
2090 is_cluster: false,
2091 label_width: leaf_label_metrics_by_id.get(&n.id).map(|v| v.0),
2092 label_height: leaf_label_metrics_by_id.get(&n.id).map(|v| v.1),
2093 });
2094 }
2095 for id in &empty_subgraph_ids {
2096 let (x, y) = base_pos
2097 .get(id)
2098 .copied()
2099 .ok_or_else(|| Error::InvalidModel {
2100 message: format!("missing positioned node {id}"),
2101 })?;
2102 let (width, height) = leaf_rects
2103 .get(id)
2104 .map(|r| (r.width(), r.height()))
2105 .ok_or_else(|| Error::InvalidModel {
2106 message: format!("missing sized node {id}"),
2107 })?;
2108 out_nodes.push(LayoutNode {
2109 id: id.clone(),
2110 x,
2111 y,
2112 width,
2113 height,
2114 is_cluster: false,
2115 label_width: leaf_label_metrics_by_id.get(id).map(|v| v.0),
2116 label_height: leaf_label_metrics_by_id.get(id).map(|v| v.1),
2117 });
2118 }
2119 for id in &self_loop_label_node_ids {
2120 let (x, y) = base_pos
2121 .get(id)
2122 .copied()
2123 .ok_or_else(|| Error::InvalidModel {
2124 message: format!("missing positioned node {id}"),
2125 })?;
2126 let (width, height) = leaf_rects
2127 .get(id)
2128 .map(|r| (r.width(), r.height()))
2129 .ok_or_else(|| Error::InvalidModel {
2130 message: format!("missing sized node {id}"),
2131 })?;
2132 out_nodes.push(LayoutNode {
2133 id: id.clone(),
2134 x,
2135 y,
2136 width,
2137 height,
2138 is_cluster: false,
2139 label_width: None,
2140 label_height: None,
2141 });
2142 }
2143
2144 let mut clusters: Vec<LayoutCluster> = Vec::new();
2145
2146 let mut cluster_rects: std::collections::HashMap<String, Rect> =
2147 std::collections::HashMap::new();
2148 let mut cluster_base_widths: std::collections::HashMap<String, f64> =
2149 std::collections::HashMap::new();
2150 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
2151
2152 struct ClusterRectContext<'a> {
2153 subgraphs_by_id: &'a std::collections::HashMap<String, FlowSubgraph>,
2154 leaf_rects: &'a std::collections::HashMap<String, Rect>,
2155 extra_children: &'a std::collections::HashMap<String, Vec<String>>,
2156 measurer: &'a dyn TextMeasurer,
2157 text_style: &'a TextStyle,
2158 html_label_text_style: &'a TextStyle,
2159 title_wrapping_width: f64,
2160 wrap_mode: WrapMode,
2161 config: &'a MermaidConfig,
2162 math_renderer: Option<&'a (dyn MathRenderer + Send + Sync)>,
2163 cluster_padding: f64,
2164 title_total_margin: f64,
2165 }
2166
2167 struct ClusterRectState<'a> {
2168 cluster_rects: &'a mut std::collections::HashMap<String, Rect>,
2169 cluster_base_widths: &'a mut std::collections::HashMap<String, f64>,
2170 visiting: &'a mut std::collections::HashSet<String>,
2171 }
2172
2173 fn compute_cluster_rect(
2174 id: &str,
2175 ctx: &ClusterRectContext<'_>,
2176 state: &mut ClusterRectState<'_>,
2177 ) -> Result<(Rect, f64)> {
2178 if let Some(r) = state.cluster_rects.get(id).copied() {
2179 let base_width = state
2180 .cluster_base_widths
2181 .get(id)
2182 .copied()
2183 .unwrap_or_else(|| r.width());
2184 return Ok((r, base_width));
2185 }
2186 if !state.visiting.insert(id.to_string()) {
2187 return Err(Error::InvalidModel {
2188 message: format!("cycle in subgraph membership involving {id}"),
2189 });
2190 }
2191
2192 let Some(sg) = ctx.subgraphs_by_id.get(id) else {
2193 return Err(Error::InvalidModel {
2194 message: format!("missing subgraph definition for {id}"),
2195 });
2196 };
2197
2198 let mut content: Option<Rect> = None;
2199 for member in &sg.nodes {
2200 let member_rect = if let Some(r) = ctx.leaf_rects.get(member).copied() {
2201 Some(r)
2202 } else if ctx.subgraphs_by_id.contains_key(member) {
2203 Some(compute_cluster_rect(member, ctx, state)?.0)
2204 } else {
2205 None
2206 };
2207
2208 if let Some(r) = member_rect {
2209 if let Some(ref mut cur) = content {
2210 cur.union(r);
2211 } else {
2212 content = Some(r);
2213 }
2214 }
2215 }
2216
2217 if let Some(extra) = ctx.extra_children.get(id) {
2218 for child in extra {
2219 if let Some(r) = ctx.leaf_rects.get(child).copied() {
2220 if let Some(ref mut cur) = content {
2221 cur.union(r);
2222 } else {
2223 content = Some(r);
2224 }
2225 }
2226 }
2227 }
2228
2229 let label_type = sg.label_type.as_deref().unwrap_or("text");
2230 let title_width_limit = Some(ctx.title_wrapping_width);
2231 let title_metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
2232 measurer: ctx.measurer,
2233 raw_label: &sg.title,
2234 label_type,
2235 style: if ctx.wrap_mode == WrapMode::HtmlLike {
2236 ctx.html_label_text_style
2237 } else {
2238 ctx.text_style
2239 },
2240 max_width_px: title_width_limit,
2241 wrap_mode: ctx.wrap_mode,
2242 config: ctx.config,
2243 math_renderer: ctx.math_renderer,
2244 preserve_string_whitespace_height: false,
2245 });
2246 let mut rect = if let Some(r) = content {
2247 r
2248 } else {
2249 Rect::from_center(
2250 0.0,
2251 0.0,
2252 title_metrics.width.max(1.0),
2253 title_metrics.height.max(1.0),
2254 )
2255 };
2256
2257 rect.pad(ctx.cluster_padding);
2259
2260 let base_width = rect.width();
2263
2264 let min_width = title_metrics.width.max(1.0) + ctx.cluster_padding;
2268 if rect.width() < min_width {
2269 let (cx, cy) = rect.center();
2270 rect = Rect::from_center(cx, cy, min_width, rect.height());
2271 }
2272
2273 if ctx.title_total_margin > 0.0 {
2275 let (cx, cy) = rect.center();
2276 rect = Rect::from_center(cx, cy, rect.width(), rect.height() + ctx.title_total_margin);
2277 }
2278
2279 let min_height = title_metrics.height.max(1.0) + ctx.title_total_margin;
2281 if rect.height() < min_height {
2282 let (cx, cy) = rect.center();
2283 rect = Rect::from_center(cx, cy, rect.width(), min_height);
2284 }
2285
2286 state.visiting.remove(id);
2287 state.cluster_rects.insert(id.to_string(), rect);
2288 state.cluster_base_widths.insert(id.to_string(), base_width);
2289 Ok((rect, base_width))
2290 }
2291
2292 struct ClusterTitleAdjustContext<'a> {
2293 measurer: &'a dyn TextMeasurer,
2294 text_style: &'a TextStyle,
2295 html_label_text_style: &'a TextStyle,
2296 title_wrapping_width: f64,
2297 wrap_mode: WrapMode,
2298 config: &'a MermaidConfig,
2299 math_renderer: Option<&'a (dyn MathRenderer + Send + Sync)>,
2300 title_total_margin: f64,
2301 cluster_padding: f64,
2302 }
2303
2304 fn adjust_cluster_rect_for_title(
2305 mut rect: Rect,
2306 title: &str,
2307 label_type: &str,
2308 add_title_total_margin: bool,
2309 ctx: &ClusterTitleAdjustContext<'_>,
2310 ) -> Rect {
2311 let title_width_limit = Some(ctx.title_wrapping_width);
2312 let title_metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
2313 measurer: ctx.measurer,
2314 raw_label: title,
2315 label_type,
2316 style: if ctx.wrap_mode == WrapMode::HtmlLike {
2317 ctx.html_label_text_style
2318 } else {
2319 ctx.text_style
2320 },
2321 max_width_px: title_width_limit,
2322 wrap_mode: ctx.wrap_mode,
2323 config: ctx.config,
2324 math_renderer: ctx.math_renderer,
2325 preserve_string_whitespace_height: false,
2326 });
2327 let title_w = title_metrics.width.max(1.0);
2328 let title_h = title_metrics.height.max(1.0);
2329
2330 let min_w = title_w + ctx.cluster_padding;
2333 if rect.width() < min_w {
2334 let (cx, cy) = rect.center();
2335 rect = Rect::from_center(cx, cy, min_w, rect.height());
2336 }
2337
2338 if add_title_total_margin && ctx.title_total_margin > 0.0 {
2340 let (cx, cy) = rect.center();
2341 rect = Rect::from_center(cx, cy, rect.width(), rect.height() + ctx.title_total_margin);
2342 }
2343
2344 let min_h = title_h + ctx.title_total_margin;
2346 if rect.height() < min_h {
2347 let (cx, cy) = rect.center();
2348 rect = Rect::from_center(cx, cy, rect.width(), min_h);
2349 }
2350
2351 rect
2352 }
2353
2354 let cluster_rect_ctx = ClusterRectContext {
2355 subgraphs_by_id: &subgraphs_by_id,
2356 leaf_rects: &leaf_rects,
2357 extra_children: &extra_children,
2358 measurer,
2359 text_style: &text_style,
2360 html_label_text_style: &html_label_text_style,
2361 title_wrapping_width: cluster_title_wrapping_width,
2362 wrap_mode: cluster_wrap_mode,
2363 config: effective_config,
2364 math_renderer,
2365 cluster_padding,
2366 title_total_margin,
2367 };
2368 let mut cluster_rect_state = ClusterRectState {
2369 cluster_rects: &mut cluster_rects,
2370 cluster_base_widths: &mut cluster_base_widths,
2371 visiting: &mut visiting,
2372 };
2373 let title_adjust_ctx = ClusterTitleAdjustContext {
2374 measurer,
2375 text_style: &text_style,
2376 html_label_text_style: &html_label_text_style,
2377 title_wrapping_width: cluster_title_wrapping_width,
2378 wrap_mode: cluster_wrap_mode,
2379 config: effective_config,
2380 math_renderer,
2381 title_total_margin,
2382 cluster_padding,
2383 };
2384
2385 for sg in &model.subgraphs {
2386 if sg.nodes.is_empty() {
2387 continue;
2388 }
2389
2390 let (rect, base_width) = if extracted_graphs.contains_key(&sg.id) {
2391 let rect = extracted_cluster_rects
2395 .get(&sg.id)
2396 .copied()
2397 .unwrap_or_else(|| {
2398 compute_cluster_rect(&sg.id, &cluster_rect_ctx, &mut cluster_rect_state)
2399 .map(|v| v.0)
2400 .unwrap_or_else(|_| Rect::from_center(0.0, 0.0, 1.0, 1.0))
2401 });
2402 let base_width = extracted_cluster_base_widths
2403 .get(&sg.id)
2404 .copied()
2405 .unwrap_or_else(|| rect.width());
2406 let rect = adjust_cluster_rect_for_title(
2407 rect,
2408 &sg.title,
2409 sg.label_type.as_deref().unwrap_or("text"),
2410 false,
2411 &title_adjust_ctx,
2412 );
2413 (rect, base_width)
2414 } else if let Some(r) = cluster_rects_from_graph.get(&sg.id).copied() {
2415 let base_width = r.width();
2416 let rect = adjust_cluster_rect_for_title(
2417 r,
2418 &sg.title,
2419 sg.label_type.as_deref().unwrap_or("text"),
2420 true,
2421 &title_adjust_ctx,
2422 );
2423 (rect, base_width)
2424 } else {
2425 compute_cluster_rect(&sg.id, &cluster_rect_ctx, &mut cluster_rect_state)?
2426 };
2427 let (cx, cy) = rect.center();
2428
2429 let label_type = sg.label_type.as_deref().unwrap_or("text");
2430 let title_width_limit = Some(cluster_title_wrapping_width);
2431 let mut title_metrics = flowchart_label_metrics_for_layout(FlowchartLabelMetricsRequest {
2432 measurer,
2433 raw_label: &sg.title,
2434 label_type,
2435 style: if cluster_wrap_mode == WrapMode::HtmlLike {
2436 &html_label_text_style
2437 } else {
2438 &text_style
2439 },
2440 max_width_px: title_width_limit,
2441 wrap_mode: cluster_wrap_mode,
2442 config: effective_config,
2443 math_renderer,
2444 preserve_string_whitespace_height: false,
2445 });
2446 if cluster_wrap_mode == crate::text::WrapMode::SvgLike && label_type != "markdown" {
2447 let plain = crate::flowchart::flowchart_label_plain_text_for_layout(
2452 &sg.title, label_type, false,
2453 );
2454 title_metrics.width = flowchart_svg_plain_computed_width_px(
2455 measurer,
2456 &plain,
2457 &text_style,
2458 title_width_limit,
2459 );
2460 } else if cluster_wrap_mode == crate::text::WrapMode::SvgLike && label_type == "markdown" {
2461 let has_emphasis = crate::text::mermaid_markdown_to_wrapped_word_lines(
2466 measurer,
2467 &sg.title,
2468 &text_style,
2469 title_width_limit,
2470 cluster_wrap_mode,
2471 )
2472 .iter()
2473 .any(|line| {
2474 line.iter()
2475 .any(|(_, ty)| *ty == crate::text::MermaidMarkdownWordType::Em)
2476 });
2477 if has_emphasis {
2478 title_metrics.width = crate::text::measure_markdown_svg_like_precise_width_px(
2479 measurer,
2480 &sg.title,
2481 &text_style,
2482 title_width_limit,
2483 );
2484 }
2485 }
2486 let title_label = LayoutLabel {
2487 x: cx,
2488 y: cy - rect.height() / 2.0 + title_margin_top + title_metrics.height / 2.0,
2489 width: title_metrics.width,
2490 height: title_metrics.height,
2491 };
2492
2493 let title_w = title_metrics.width.max(1.0);
2498 let diff = if base_width <= title_w {
2499 (title_w - base_width) / 2.0 - cluster_padding / 2.0
2500 } else {
2501 -cluster_padding / 2.0
2502 };
2503 let offset_y = title_metrics.height - cluster_padding / 2.0;
2504
2505 let effective_dir = effective_dir_by_id
2506 .get(&sg.id)
2507 .cloned()
2508 .unwrap_or_else(|| effective_cluster_dir(sg, &diagram_direction, inherit_dir));
2509
2510 clusters.push(LayoutCluster {
2511 id: sg.id.clone(),
2512 x: cx,
2513 y: cy,
2514 width: rect.width(),
2515 height: rect.height(),
2516 diff,
2517 offset_y,
2518 title: sg.title.clone(),
2519 title_label,
2520 requested_dir: sg.dir.as_ref().map(|s| normalize_dir(s)),
2521 effective_dir,
2522 padding: cluster_padding,
2523 title_margin_top,
2524 title_margin_bottom,
2525 });
2526
2527 out_nodes.push(LayoutNode {
2528 id: sg.id.clone(),
2529 x: cx,
2530 y: cy,
2532 width: rect.width(),
2533 height: rect.height(),
2534 is_cluster: true,
2535 label_width: None,
2536 label_height: None,
2537 });
2538 }
2539 clusters.sort_by(|a, b| a.id.cmp(&b.id));
2540
2541 let mut out_edges: Vec<LayoutEdge> = Vec::new();
2542 for e in &render_edges {
2543 let (points, label_pos, mut from_cluster, mut to_cluster) =
2544 if let Some(points) = edge_override_points.get(&e.id) {
2545 let from_cluster = edge_override_from_cluster
2546 .get(&e.id)
2547 .cloned()
2548 .unwrap_or(None);
2549 let to_cluster = edge_override_to_cluster.get(&e.id).cloned().unwrap_or(None);
2550 (
2551 points.clone(),
2552 edge_override_label.get(&e.id).cloned().unwrap_or(None),
2553 from_cluster,
2554 to_cluster,
2555 )
2556 } else {
2557 let (v, w) = edge_endpoints_by_id
2558 .get(&e.id)
2559 .cloned()
2560 .unwrap_or_else(|| (e.from.clone(), e.to.clone()));
2561 let edge_key = edge_key_by_id
2562 .get(&e.id)
2563 .map(String::as_str)
2564 .unwrap_or(e.id.as_str());
2565 let Some(label) = g.edge(&v, &w, Some(edge_key)) else {
2566 return Err(Error::InvalidModel {
2567 message: format!("missing layout edge {}", e.id),
2568 });
2569 };
2570 let from_cluster = label
2571 .extras
2572 .get("fromCluster")
2573 .and_then(|v| v.as_str().map(|s| s.to_string()));
2574 let to_cluster = label
2575 .extras
2576 .get("toCluster")
2577 .and_then(|v| v.as_str().map(|s| s.to_string()));
2578 let points = label
2579 .points
2580 .iter()
2581 .map(|p| LayoutPoint { x: p.x, y: p.y })
2582 .collect::<Vec<_>>();
2583 let label_pos = match (label.x, label.y) {
2584 (Some(x), Some(y)) if label.width > 0.0 || label.height > 0.0 => {
2585 Some(LayoutLabel {
2586 x,
2587 y,
2588 width: label.width,
2589 height: label.height,
2590 })
2591 }
2592 _ => None,
2593 };
2594 (points, label_pos, from_cluster, to_cluster)
2595 };
2596
2597 if subgraph_ids.contains(e.from.as_str()) && e.id.ends_with("-cyclic-special-1") {
2601 from_cluster = Some(e.from.clone());
2602 }
2603 if subgraph_ids.contains(e.to.as_str()) && e.id.ends_with("-cyclic-special-2") {
2604 to_cluster = Some(e.to.clone());
2605 }
2606
2607 out_edges.push(LayoutEdge {
2608 id: e.id.clone(),
2609 from: e.from.clone(),
2610 to: e.to.clone(),
2611 from_cluster,
2612 to_cluster,
2613 points,
2614 label: label_pos,
2615 start_label_left: None,
2616 start_label_right: None,
2617 end_label_left: None,
2618 end_label_right: None,
2619 start_marker: None,
2620 end_marker: None,
2621 stroke_dasharray: None,
2622 });
2623 }
2624
2625 let mut node_shape_by_id: HashMap<&str, &str> = HashMap::new();
2632 for n in &model.nodes {
2633 if let Some(s) = n.layout_shape.as_deref() {
2634 node_shape_by_id.insert(n.id.as_str(), s);
2635 }
2636 }
2637 let mut layout_node_by_id: HashMap<&str, &LayoutNode> = HashMap::new();
2638 for n in &out_nodes {
2639 layout_node_by_id.insert(n.id.as_str(), n);
2640 }
2641
2642 fn diamond_intersection(node: &LayoutNode, toward: &LayoutPoint) -> Option<LayoutPoint> {
2643 let vx = toward.x - node.x;
2644 let vy = toward.y - node.y;
2645 if !(vx.is_finite() && vy.is_finite()) {
2646 return None;
2647 }
2648 if vx.abs() <= 1e-12 && vy.abs() <= 1e-12 {
2649 return None;
2650 }
2651 let hw = (node.width / 2.0).max(1e-9);
2652 let hh = (node.height / 2.0).max(1e-9);
2653 let denom = vx.abs() / hw + vy.abs() / hh;
2654 if !(denom.is_finite() && denom > 0.0) {
2655 return None;
2656 }
2657 let t = 1.0 / denom;
2658 Some(LayoutPoint {
2659 x: node.x + vx * t,
2660 y: node.y + vy * t,
2661 })
2662 }
2663
2664 for e in &mut out_edges {
2665 if e.points.len() < 2 {
2666 continue;
2667 }
2668
2669 if let Some(node) = layout_node_by_id.get(e.from.as_str()) {
2670 if !node.is_cluster {
2671 let shape = node_shape_by_id
2672 .get(e.from.as_str())
2673 .copied()
2674 .unwrap_or("squareRect");
2675 if matches!(shape, "diamond" | "question" | "diam") {
2676 if let Some(p) = diamond_intersection(node, &e.points[1]) {
2677 e.points[0] = p;
2678 }
2679 }
2680 }
2681 }
2682 if let Some(node) = layout_node_by_id.get(e.to.as_str()) {
2683 if !node.is_cluster {
2684 let shape = node_shape_by_id
2685 .get(e.to.as_str())
2686 .copied()
2687 .unwrap_or("squareRect");
2688 if matches!(shape, "diamond" | "question" | "diam") {
2689 let n = e.points.len();
2690 if let Some(p) = diamond_intersection(node, &e.points[n - 2]) {
2691 e.points[n - 1] = p;
2692 }
2693 }
2694 }
2695 }
2696 }
2697
2698 let bounds = compute_bounds(&out_nodes, &out_edges);
2699
2700 if let Some(s) = build_output_start {
2701 timings.build_output = s.elapsed();
2702 }
2703 if let Some(s) = total_start {
2704 timings.total = s.elapsed();
2705 let dagre_overhead = timings
2706 .layout_recursive
2707 .checked_sub(timings.dagre_total)
2708 .unwrap_or_default();
2709 eprintln!(
2710 "[layout-timing] diagram=flowchart-v2 total={:?} deserialize={:?} expand_self_loops={:?} build_graph={:?} extract_clusters={:?} dom_order={:?} layout_recursive={:?} dagre_calls={} dagre_total={:?} dagre_overhead={:?} place_graph={:?} build_output={:?}",
2711 timings.total,
2712 timings.deserialize,
2713 timings.expand_self_loops,
2714 timings.build_graph,
2715 timings.extract_clusters,
2716 timings.dom_order,
2717 timings.layout_recursive,
2718 timings.dagre_calls,
2719 timings.dagre_total,
2720 dagre_overhead,
2721 timings.place_graph,
2722 timings.build_output,
2723 );
2724 }
2725
2726 Ok(FlowchartV2Layout {
2727 nodes: out_nodes,
2728 edges: out_edges,
2729 clusters,
2730 bounds,
2731 dom_node_order_by_root,
2732 })
2733}
2734
2735#[cfg(test)]
2736mod tests {
2737 use super::*;
2738 use std::sync::mpsc;
2739 use std::time::Duration;
2740
2741 #[test]
2742 fn extract_descendants_handles_deeply_nested_subgraphs() {
2743 const DEPTH: usize = 100_000;
2744 let (tx, rx) = mpsc::channel();
2745 std::thread::Builder::new()
2746 .stack_size(512 * 1024)
2747 .spawn(move || {
2748 let mut g = Graph::new(GraphOptions {
2749 compound: true,
2750 ..Default::default()
2751 });
2752 for i in (0..DEPTH).rev() {
2753 g.set_parent(format!("n{}", i + 1), format!("n{i}"));
2754 }
2755 let _ = tx.send(extract_descendants("n0", &g));
2756 })
2757 .unwrap();
2758 let descendants = rx
2759 .recv_timeout(Duration::from_secs(2))
2760 .expect("extract_descendants overflowed the stack on deep nesting");
2761 assert_eq!(descendants.len(), DEPTH);
2762 }
2763}