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