use super::constants::*;
use super::parser::{EdgeStyle, FlowchartDiagram, NodeShape, NodeStyle, Subgraph};
use super::templates;
use crate::svg::SvgWriter;
use crate::text::measure;
use crate::theme::{Theme, ThemeVars};
use dagre_dgl_rs::graph::{EdgeLabel, Graph, GraphLabel, NodeLabel, Point};
use dagre_dgl_rs::layout::layout;
use std::collections::{HashMap, HashSet};
pub fn render(diag: &FlowchartDiagram, theme: Theme, use_foreign_object: bool) -> String {
let vars = theme.resolve();
let subgraph_ids: HashSet<String> = diag.subgraphs.iter().map(|sg| sg.id.clone()).collect();
let mut sg_parent: HashMap<String, String> = HashMap::new();
for sg in &diag.subgraphs {
for member in &sg.members {
if subgraph_ids.contains(member) {
sg_parent.insert(member.clone(), sg.id.clone());
}
}
}
let sg_descendants: HashMap<String, HashSet<String>> = diag
.subgraphs
.iter()
.map(|sg| {
(
sg.id.clone(),
collect_descendants(&sg.id, &subgraph_ids, diag),
)
})
.collect();
let mut sg_external: HashSet<String> = HashSet::new();
for edge in &diag.edges {
for sg in &diag.subgraphs {
let descs = sg_descendants.get(&sg.id).unwrap();
let v_in = descs.contains(&edge.from);
let w_in = descs.contains(&edge.to);
if v_in ^ w_in {
sg_external.insert(sg.id.clone());
}
}
}
let rankdir = match diag.direction.as_str() {
"LR" => "LR",
"RL" => "RL",
"BT" => "BT",
_ => "TB",
};
let top_sg_ids: Vec<String> = diag
.subgraphs
.iter()
.filter(|sg| !sg_parent.contains_key(&sg.id))
.map(|sg| sg.id.clone())
.collect();
let outer_ranksep = OUTER_RANKSEP;
let mut sg_layouts: HashMap<String, SgLayout> = HashMap::new();
for sg_id in &top_sg_ids {
if !sg_external.contains(sg_id) {
let layout_result = compute_sg_layout(
sg_id,
diag,
&subgraph_ids,
rankdir,
outer_ranksep,
&mut sg_layouts,
);
sg_layouts.insert(sg_id.clone(), layout_result);
}
}
let mut g = Graph::with_options(true, true, true); g.set_graph(GraphLabel {
rankdir: Some(rankdir.to_string()),
nodesep: Some(NODE_SEP),
ranksep: Some(outer_ranksep),
marginx: Some(GRAPH_MARGIN),
marginy: Some(GRAPH_MARGIN),
..Default::default()
});
for sg_id in &top_sg_ids {
if sg_external.contains(sg_id) {
g.set_node(
sg_id,
NodeLabel {
width: 0.0,
height: 0.0,
..Default::default()
},
);
}
}
for (id, node) in &diag.nodes {
if subgraph_ids.contains(id) {
continue;
}
let top_level_ancestor = if let Some(direct_parent) = diag.node_subgraph.get(id) {
let mut cur = direct_parent.clone();
loop {
match sg_parent.get(&cur) {
None => break,
Some(p) => cur = p.clone(),
}
}
Some(cur)
} else {
None
};
let in_outer = match &top_level_ancestor {
Some(ancestor) => sg_external.contains(ancestor),
None => true,
};
if in_outer {
let (w, h) = node_size(&node.label, &node.shape);
let intersect_type = shape_intersect_type(&node.shape);
g.set_node(
id,
NodeLabel {
width: w,
height: h,
intersect_type,
..Default::default()
},
);
if let Some(ancestor) = &top_level_ancestor {
if sg_external.contains(ancestor) {
g.set_parent(id, Some(ancestor));
}
}
}
}
let sg_layout_margin = SG_LAYOUT_MARGIN;
for sg_id in &top_sg_ids {
if !sg_external.contains(sg_id) {
if let Some(sg_layout) = sg_layouts.get(sg_id) {
g.set_node(
sg_id,
NodeLabel {
width: sg_layout.width.max(1.0),
height: sg_layout.height.max(1.0),
..Default::default()
},
);
}
}
}
for edge in &diag.edges {
let from_outer = map_to_outer_node(
&edge.from,
&top_sg_ids,
&subgraph_ids,
&sg_external,
&sg_parent,
&sg_descendants,
diag,
);
let to_outer = map_to_outer_node(
&edge.to,
&top_sg_ids,
&subgraph_ids,
&sg_external,
&sg_parent,
&sg_descendants,
diag,
);
if let (Some(from_n), Some(to_n)) = (from_outer, to_outer) {
if from_n != to_n && g.has_node(&from_n) && g.has_node(&to_n) {
let (lbl_w, lbl_h) = if let Some(lbl) = edge.label.as_deref() {
if !lbl.is_empty() {
let (tw, _) = measure(lbl, FONT_SIZE);
(tw, LABEL_FO_HEIGHT)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
g.set_edge(
&from_n,
&to_n,
EdgeLabel {
minlen: Some(1),
weight: Some(1.0),
width: Some(lbl_w),
height: Some(lbl_h),
..Default::default()
},
None,
);
}
}
}
layout(&mut g);
let mut node_global: HashMap<String, (f64, f64, f64, f64)> = HashMap::new();
let mut sg_global_bounds_map: HashMap<String, (f64, f64, f64, f64)> = HashMap::new();
let mut edge_global: Vec<(String, String, Vec<Point>)> = Vec::new();
let mut all_sg_full_sizes: HashMap<String, (f64, f64)> = HashMap::new();
fn collect_full_sizes(
sg_id: &str,
sg_layout: &SgLayout,
all_sg_layouts: &HashMap<String, SgLayout>,
margin2: f64,
out: &mut HashMap<String, (f64, f64)>,
) {
out.insert(
sg_id.to_string(),
(sg_layout.width + margin2, sg_layout.height + margin2),
);
for (child_id, &(full_w, full_h)) in &sg_layout.child_sg_full_sizes {
out.insert(child_id.clone(), (full_w, full_h));
if let Some(child_layout) = all_sg_layouts.get(child_id) {
collect_full_sizes(child_id, child_layout, all_sg_layouts, margin2, out);
}
}
}
for (sg_id, sg_layout) in &sg_layouts {
collect_full_sizes(
sg_id,
sg_layout,
&sg_layouts,
sg_layout_margin,
&mut all_sg_full_sizes,
);
}
for v in g.nodes() {
if let Some(n) = g.node_opt(&v) {
if let (Some(cx), Some(cy)) = (n.x, n.y) {
if sg_external.contains(&v) {
sg_global_bounds_map.insert(v.clone(), (cx, cy, n.width, n.height));
} else if subgraph_ids.contains(&v) {
let (full_w, full_h) = all_sg_full_sizes
.get(&v)
.copied()
.unwrap_or((n.width + sg_layout_margin, n.height + sg_layout_margin));
sg_global_bounds_map.insert(v.clone(), (cx, cy, full_w, full_h));
if let Some(sg_layout) = sg_layouts.get(&v) {
let sg_ox = cx - full_w / 2.0;
let sg_oy = cy - full_h / 2.0;
composite_sg_layout(
&v,
sg_layout,
&sg_layouts,
&subgraph_ids,
diag,
sg_ox,
sg_oy,
&mut node_global,
&mut sg_global_bounds_map,
&all_sg_full_sizes,
sg_layout_margin,
&mut edge_global,
);
}
} else {
node_global.insert(v.clone(), (cx, cy, n.width, n.height));
}
}
}
}
let sg_bounds: HashMap<String, (f64, f64, f64, f64)> = sg_global_bounds_map.clone();
let mut g_full = Graph::with_options(true, true, false);
for (id, &(cx, cy, w, h)) in &node_global {
let mut nl = NodeLabel {
width: w,
height: h,
..Default::default()
};
nl.x = Some(cx);
nl.y = Some(cy);
g_full.set_node(id, nl);
}
for (sg_id, &(cx, cy, w, h)) in &sg_global_bounds_map {
if !g_full.has_node(sg_id) {
let mut nl = NodeLabel {
width: w,
height: h,
..Default::default()
};
nl.x = Some(cx);
nl.y = Some(cy);
g_full.set_node(sg_id, nl);
}
}
for e in g.edges() {
if let Some(lbl) = g.edge(&e) {
g_full.set_edge_obj(&e, lbl.clone());
}
}
for (from, to, pts) in edge_global {
let _e = dagre_dgl_rs::graph::Edge::new(&from, &to);
if !g_full.has_edge(&from, &to) {
g_full.set_edge(
&from,
&to,
EdgeLabel {
points: Some(pts),
..Default::default()
},
None,
);
} else if let Some(lbl) = g_full.edge_vw_mut(&from, &to) {
lbl.points = Some(pts);
}
}
let (graph_w, graph_h) = {
let margin_x = GRAPH_MARGIN;
let margin_y = GRAPH_MARGIN;
let mut max_x = 0.0_f64;
let mut max_y = 0.0_f64;
for &(cx, cy, w, h) in node_global.values() {
max_x = max_x.max(cx + w / 2.0);
max_y = max_y.max(cy + h / 2.0);
}
for (sg_id, &(cx, cy, w, h)) in &sg_global_bounds_map {
let (bw, bh) = if sg_external.contains(sg_id) {
(w, h)
} else {
(
(w - sg_layout_margin).max(1.0),
(h - sg_layout_margin).max(1.0),
)
};
max_x = max_x.max(cx + bw / 2.0);
max_y = max_y.max(cy + bh / 2.0);
}
(max_x + margin_x, max_y + margin_y)
};
let g = g_full;
let svg_id = "mermaid-svg";
let css = build_css(svg_id, &vars);
let mut w = SvgWriter::with_capacity(32_768);
w.raw(&templates::svg_root(svg_id, graph_w, graph_w, graph_h));
w.raw("<style>").raw(&css).raw("</style>");
w.raw("<g>")
.raw(&templates::all_markers(svg_id))
.raw("</g>");
w.raw(r#"<g class="root">"#);
let top_level_sgs: Vec<&Subgraph> = diag
.subgraphs
.iter()
.filter(|sg| !sg_parent.contains_key(&sg.id))
.collect();
let out_buf = w.finish();
let mut out = out_buf;
render_root_group(
&mut out,
svg_id,
diag,
&g,
&subgraph_ids,
&sg_parent,
&diag.node_subgraph,
&sg_bounds,
&sg_external,
&sg_descendants,
&top_level_sgs,
None, None, use_foreign_object,
&vars,
);
let mut w2 = SvgWriter::with_capacity(out.len() + 4096);
w2.raw(&out);
w2.raw("</g>"); w2.raw(&templates::drop_shadow_filter(svg_id));
w2.raw(&templates::drop_shadow_filter_small(svg_id));
w2.raw("</svg>");
w2.finish()
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::only_used_in_recursion)]
fn render_root_group(
out: &mut String,
svg_id: &str,
diag: &FlowchartDiagram,
g: &Graph,
subgraph_ids: &HashSet<String>,
sg_parent: &HashMap<String, String>,
node_subgraph: &HashMap<String, String>,
sg_bounds: &HashMap<String, (f64, f64, f64, f64)>,
sg_external: &HashSet<String>,
sg_descendants: &HashMap<String, HashSet<String>>,
context_sgs: &[&Subgraph],
self_cluster: Option<&Subgraph>,
parent_offset: Option<(f64, f64)>,
use_foreign_object: bool,
vars: &ThemeVars,
) {
let (off_x, off_y) = parent_offset.unwrap_or((0.0, 0.0));
let ff = vars.font_family;
let external_here: Vec<&Subgraph> = context_sgs
.iter()
.copied()
.filter(|sg| sg_external.contains(&sg.id))
.collect();
let internal_here: Vec<&Subgraph> = context_sgs
.iter()
.copied()
.filter(|sg| !sg_external.contains(&sg.id))
.collect();
out.push_str(r#"<g class="clusters">"#);
if let Some(sc) = self_cluster {
if let Some(&(_, _, comp_w, comp_h)) = sg_bounds.get(&sc.id) {
let rect_x = GRAPH_MARGIN;
let rect_y = GRAPH_MARGIN;
let rect_w = comp_w - SG_LAYOUT_MARGIN;
let rect_h = comp_h - SG_LAYOUT_MARGIN;
let local_cx = comp_w / 2.0;
out.push_str(&render_cluster_rect(
svg_id,
sc,
rect_x,
rect_y,
local_cx,
rect_w,
rect_h,
use_foreign_object,
));
}
}
for sg in &external_here {
if let Some(&(abs_cx, abs_cy, w, h)) = sg_bounds.get(&sg.id) {
let local_cx = abs_cx - off_x;
let local_cy = abs_cy - off_y;
let lx = local_cx - w / 2.0;
let ly = local_cy - h / 2.0;
out.push_str(&render_cluster_rect(
svg_id,
sg,
lx,
ly,
local_cx,
w,
h,
use_foreign_object,
));
}
}
out.push_str("</g>");
out.push_str(r#"<g class="edgePaths">"#);
for (i, edge) in diag.edges.iter().enumerate() {
let from_key = resolve_edge_endpoint(
&edge.from,
subgraph_ids,
diag,
g,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
);
let to_key = resolve_edge_endpoint(
&edge.to,
subgraph_ids,
diag,
g,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
);
let (from_key, to_key) = match (from_key, to_key) {
(Some(f), Some(t)) if f != t => (f, t),
_ => continue,
};
let e = dagre_dgl_rs::graph::Edge::new(&from_key, &to_key);
if let Some(lbl) = g.edge(&e) {
let pts = lbl.points.clone().unwrap_or_default();
if pts.len() >= 2 {
let mut local_pts: Vec<Point> = pts
.iter()
.map(|p| Point {
x: p.x - off_x,
y: p.y - off_y,
})
.collect();
if subgraph_ids.contains(&edge.from) && sg_external.contains(&edge.from) {
if let Some(&(cx, cy, w, h)) = sg_bounds.get(&edge.from) {
let lcx = cx - off_x;
let lcy = cy - off_y;
local_pts = clip_path_from_cluster(&local_pts, lcx, lcy, w, h, true);
}
}
if subgraph_ids.contains(&edge.to) && sg_external.contains(&edge.to) {
if let Some(&(cx, cy, w, h)) = sg_bounds.get(&edge.to) {
let lcx = cx - off_x;
let lcy = cy - off_y;
local_pts = clip_path_from_cluster(&local_pts, lcx, lcy, w, h, false);
}
}
let edge_id = format!("{}-L_{}_{}_{}", svg_id, edge.from, edge.to, i);
let is_thick = matches!(edge.style, EdgeStyle::ThickArrow);
let is_dashed = matches!(edge.style, EdgeStyle::DotArrow | EdgeStyle::DotLine);
let has_arrow = !matches!(edge.style, EdgeStyle::Line | EdgeStyle::DotLine);
let is_cross = matches!(edge.style, EdgeStyle::CrossArrow);
let is_open = matches!(edge.style, EdgeStyle::OpenArrow);
let end_trim = if has_arrow && !is_cross && !is_open {
POINT_END_TRIM
} else {
0.0
};
let trimmed = trim_path_end(&local_pts, end_trim);
let path_d = edge_path(&trimmed);
let classes = if is_thick {
" edge-thickness-thick edge-pattern-solid flowchart-link"
} else if is_dashed {
" edge-thickness-normal edge-pattern-dashed flowchart-link"
} else {
" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link"
};
let marker_end = if is_cross {
templates::marker_end_cross(svg_id)
} else if is_open {
templates::marker_end_circle(svg_id)
} else if has_arrow {
templates::marker_end_point(svg_id)
} else {
String::new()
};
out.push_str(&templates::edge_path(
&path_d,
&edge_id,
classes,
&marker_end,
));
}
}
}
out.push_str("</g>");
out.push_str(r#"<g class="edgeLabels">"#);
for (i, edge) in diag.edges.iter().enumerate() {
let from_key = resolve_edge_endpoint(
&edge.from,
subgraph_ids,
diag,
g,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
);
let to_key = resolve_edge_endpoint(
&edge.to,
subgraph_ids,
diag,
g,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
);
let (from_key, to_key) = match (from_key, to_key) {
(Some(f), Some(t)) if f != t => (f, t),
_ => continue,
};
let e = dagre_dgl_rs::graph::Edge::new(&from_key, &to_key);
if let Some(lbl_data) = g.edge(&e) {
let pts = lbl_data.points.clone().unwrap_or_default();
let edge_id = format!("{}-L_{}_{}_{}", svg_id, edge.from, edge.to, i);
match edge.label.as_deref() {
Some(lbl_text) if !lbl_text.is_empty() => {
let mid_abs = midpoint(&pts);
let mx = mid_abs.0 - off_x;
let my = mid_abs.1 - off_y;
let (fo_w_raw, _) = measure(lbl_text, FONT_SIZE);
let fo_w = fo_w_raw * TEXT_SCALE;
if use_foreign_object {
out.push_str(&templates::edge_label_fo(
&fmt(mx),
&fmt(my),
&edge_id,
&fmt(-fo_w / 2.0),
&fmt(fo_w),
LABEL_FO_HEIGHT,
LABEL_Y_OFFSET,
&esc(lbl_text),
));
} else {
out.push_str(&templates::edge_label_text(
&fmt(mx),
&fmt(my),
&fmt(-fo_w / 2.0),
&fmt(fo_w),
LABEL_FO_HEIGHT,
LABEL_Y_OFFSET,
TEXT_LABEL_Y,
ff,
FONT_SIZE,
&esc(lbl_text),
));
}
}
_ => {
out.push_str(&templates::edge_label_empty(&edge_id));
}
}
}
}
out.push_str("</g>");
out.push_str(r#"<g class="nodes">"#);
for sg in &internal_here {
if let Some(&(abs_cx, abs_cy, comp_w, comp_h)) = sg_bounds.get(&sg.id) {
let abs_ox = abs_cx - comp_w / 2.0;
let abs_oy = abs_cy - comp_h / 2.0;
let local_ox = abs_ox - off_x;
let local_oy = abs_oy - off_y;
out.push_str(&templates::subgraph_root_group(
&fmt(local_ox),
&fmt(local_oy),
));
let child_sgs: Vec<&Subgraph> = diag
.subgraphs
.iter()
.filter(|child| {
sg_parent
.get(&child.id)
.map(|p| p == &sg.id)
.unwrap_or(false)
})
.collect();
render_root_group(
out,
svg_id,
diag,
g,
subgraph_ids,
sg_parent,
node_subgraph,
sg_bounds,
sg_external,
sg_descendants,
&child_sgs,
Some(sg), Some((abs_ox, abs_oy)),
use_foreign_object,
vars,
);
out.push_str("</g>"); }
}
let mut node_idx = 0usize;
for (id, flow_node) in &diag.nodes {
if subgraph_ids.contains(id) {
node_idx += 1;
continue;
}
if !node_at_level(
id,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
) {
node_idx += 1;
continue;
}
if let Some(n) = g.node_opt(id) {
let lcx = n.x.unwrap_or(0.0) - off_x;
let lcy = n.y.unwrap_or(0.0) - off_y;
let w = n.width;
let h = n.height;
let node_style = diag.node_styles.get(id);
let node_dom_id = format!("{}-flowchart-{}-{}", svg_id, id, node_idx);
out.push_str(&render_node(
flow_node,
lcx,
lcy,
w,
h,
vars,
node_style,
&node_dom_id,
use_foreign_object,
));
}
node_idx += 1;
}
out.push_str("</g>"); }
#[allow(clippy::too_many_arguments)]
fn render_cluster_rect(
svg_id: &str,
sg: &Subgraph,
x: f64, y: f64, local_cx: f64, w: f64,
h: f64,
use_foreign_object: bool,
) -> String {
let label = sg.label.as_deref().unwrap_or(&sg.id);
let (lw_raw, _) = measure(label, FONT_SIZE);
let lw = lw_raw * TEXT_SCALE;
let label_html = if use_foreign_object {
templates::cluster_label_fo(
&fmt(local_cx - lw / 2.0),
&fmt(y),
&fmt(lw),
LABEL_FO_HEIGHT,
&esc(label),
)
} else {
templates::cluster_label_text(
&fmt(local_cx),
&fmt(y + CLUSTER_LABEL_TEXT_DY),
"Arial, sans-serif",
FONT_SIZE,
&esc(label),
)
};
templates::cluster_group(
svg_id,
&sg.id,
&fmt(x),
&fmt(y),
&fmt(w),
&fmt(h),
&label_html,
)
}
fn flip_dir(outer_rankdir: &str) -> &'static str {
match outer_rankdir {
"TB" => "LR",
"BT" => "LR",
_ => "TB",
}
}
#[derive(Clone)]
struct SgLayout {
node_positions: HashMap<String, (f64, f64, f64, f64)>,
edges: Vec<(String, String, Vec<Point>)>,
child_sg_origins: HashMap<String, (f64, f64)>,
child_sg_sizes: HashMap<String, (f64, f64)>,
child_sg_full_sizes: HashMap<String, (f64, f64)>,
width: f64,
height: f64,
}
fn compute_sg_layout(
sg_id: &str,
diag: &FlowchartDiagram,
subgraph_ids: &HashSet<String>,
parent_rankdir: &str,
parent_ranksep: f64,
all_layouts_out: &mut HashMap<String, SgLayout>,
) -> SgLayout {
let sg = diag.subgraphs.iter().find(|s| s.id == sg_id).unwrap();
let inner_rankdir: &str = if let Some(d) = sg.direction.as_deref() {
match d {
"LR" => "LR",
"RL" => "RL",
"BT" => "BT",
"TB" => "TB",
_ => flip_dir(parent_rankdir),
}
} else {
flip_dir(parent_rankdir)
};
let inner_ranksep = parent_ranksep + RANKSEP_INCREMENT;
let mut child_layouts: HashMap<String, SgLayout> = HashMap::new();
for member in &sg.members {
if subgraph_ids.contains(member) {
let child_layout = compute_sg_layout(
member,
diag,
subgraph_ids,
inner_rankdir,
inner_ranksep,
all_layouts_out,
);
all_layouts_out.insert(member.clone(), child_layout.clone());
child_layouts.insert(member.clone(), child_layout);
}
}
let mut g = Graph::with_options(true, true, true); g.set_graph(GraphLabel {
rankdir: Some(inner_rankdir.to_string()),
nodesep: Some(NODE_SEP),
ranksep: Some(inner_ranksep),
marginx: Some(GRAPH_MARGIN),
marginy: Some(GRAPH_MARGIN),
..Default::default()
});
g.set_node(
sg_id,
NodeLabel {
width: 0.0,
height: 0.0,
..Default::default()
},
);
for member in &sg.members {
if !subgraph_ids.contains(member) {
if let Some(node) = diag.nodes.get(member) {
let (w, h) = node_size(&node.label, &node.shape);
let intersect_type = shape_intersect_type(&node.shape);
g.set_node(
member,
NodeLabel {
width: w,
height: h,
intersect_type,
..Default::default()
},
);
g.set_parent(member, Some(sg_id));
}
} else {
let child = &child_layouts[member];
g.set_node(
member,
NodeLabel {
width: child.width.max(1.0),
height: child.height.max(1.0),
..Default::default()
},
);
g.set_parent(member, Some(sg_id));
}
}
let members_set: HashSet<&str> = sg.members.iter().map(|s| s.as_str()).collect();
for edge in &diag.edges {
let from_member = resolve_to_member(&edge.from, &members_set, subgraph_ids, diag);
let to_member = resolve_to_member(&edge.to, &members_set, subgraph_ids, diag);
if let (Some(from_m), Some(to_m)) = (from_member, to_member) {
if from_m != to_m {
if !g.has_node(from_m) || !g.has_node(to_m) {
continue;
}
g.set_edge(
from_m,
to_m,
EdgeLabel {
minlen: Some(1),
weight: Some(1.0),
..Default::default()
},
None,
);
}
}
}
layout(&mut g);
let (cluster_w, cluster_h) = if let Some(sg_n) = g.node_opt(sg_id) {
(sg_n.width, sg_n.height)
} else {
(100.0, 100.0)
};
let _sg_inner_cx = g.node_opt(sg_id).and_then(|n| n.x).unwrap_or(0.0);
let sg_inner_cy = g.node_opt(sg_id).and_then(|n| n.y).unwrap_or(0.0);
let mut node_positions: HashMap<String, (f64, f64, f64, f64)> = HashMap::new();
let mut child_sg_origins: HashMap<String, (f64, f64)> = HashMap::new();
let mut child_sg_sizes: HashMap<String, (f64, f64)> = HashMap::new();
let mut child_sg_full_sizes: HashMap<String, (f64, f64)> = HashMap::new();
let mut edges: Vec<(String, String, Vec<Point>)> = Vec::new();
for e in g.edges() {
if let Some(lbl) = g.edge(&e) {
if let Some(pts) = &lbl.points {
if pts.len() >= 2 {
edges.push((e.v.clone(), e.w.clone(), pts.clone()));
}
}
}
}
for member in &sg.members {
if !subgraph_ids.contains(member) {
if let Some(n) = g.node_opt(member) {
let cx = n.x.unwrap_or(0.0);
let cy = n.y.unwrap_or(0.0);
node_positions.insert(member.clone(), (cx, cy, n.width, n.height));
}
} else {
if let Some(n) = g.node_opt(member) {
let cx = n.x.unwrap_or(0.0);
let cy = n.y.unwrap_or(0.0);
let child_rect_w = n.width;
let child_rect_h = n.height;
let child_full_w = child_rect_w + SG_LAYOUT_MARGIN;
let child_full_h = child_rect_h + SG_LAYOUT_MARGIN;
child_sg_origins.insert(
member.clone(),
(cx - child_full_w / 2.0, cy - child_full_h / 2.0),
);
child_sg_sizes.insert(member.clone(), (child_rect_w, child_rect_h));
child_sg_full_sizes.insert(member.clone(), (child_full_w, child_full_h));
}
}
}
if inner_rankdir == "LR" || inner_rankdir == "RL" {
let all_cy_values: Vec<f64> = node_positions
.values()
.map(|&(_, cy, _, _)| cy)
.chain(child_sg_origins.values().map(|&(_, oy)| oy))
.collect();
if !all_cy_values.is_empty() {
let min_cy = all_cy_values.iter().cloned().fold(f64::INFINITY, f64::min);
let max_cy = all_cy_values
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let member_cy_center = (min_cy + max_cy) / 2.0;
let cy_offset = sg_inner_cy - member_cy_center;
if cy_offset.abs() > 0.01 {
for (_, pos) in node_positions.iter_mut() {
pos.1 += cy_offset;
}
for (_, origin) in child_sg_origins.iter_mut() {
origin.1 += cy_offset;
}
for (_, _, pts) in edges.iter_mut() {
for p in pts.iter_mut() {
p.y += cy_offset;
}
}
}
}
}
SgLayout {
node_positions,
edges,
child_sg_origins,
child_sg_sizes,
child_sg_full_sizes,
width: cluster_w,
height: cluster_h,
}
}
#[allow(clippy::too_many_arguments)]
fn map_to_outer_node(
node_id: &str,
top_sg_ids: &[String],
subgraph_ids: &HashSet<String>,
sg_external: &HashSet<String>,
_sg_parent: &HashMap<String, String>,
sg_descendants: &HashMap<String, HashSet<String>>,
diag: &FlowchartDiagram,
) -> Option<String> {
if top_sg_ids.contains(&node_id.to_string()) {
if sg_external.contains(node_id) {
return collect_leaf_ids(node_id, subgraph_ids, diag)
.into_iter()
.next();
} else {
return Some(node_id.to_string());
}
}
for sg_id in top_sg_ids {
if let Some(descs) = sg_descendants.get(sg_id) {
if descs.contains(node_id) {
if sg_external.contains(sg_id) {
return Some(node_id.to_string());
} else {
return Some(sg_id.clone());
}
}
}
}
Some(node_id.to_string())
}
fn collect_descendants_direct(
sg_id: &str,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
) -> HashSet<String> {
let mut result = HashSet::new();
if let Some(sg) = diag.subgraphs.iter().find(|s| s.id == sg_id) {
for member in &sg.members {
result.insert(member.clone());
if subgraph_ids.contains(member) {
result.extend(collect_descendants_direct(member, subgraph_ids, diag));
}
}
}
result
}
fn resolve_to_member<'a>(
id: &str,
members_set: &HashSet<&'a str>,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
) -> Option<&'a str> {
if members_set.contains(id) {
return members_set.get(id).copied();
}
for &member in members_set {
if subgraph_ids.contains(member) {
let desc = collect_descendants_direct(member, subgraph_ids, diag);
if desc.contains(id) {
return Some(member);
}
}
}
None
}
#[allow(dead_code)]
#[allow(clippy::only_used_in_recursion)]
#[allow(clippy::too_many_arguments)]
fn composite_layouts(
top_g: &Graph,
_top_level_sgs: &[String],
sg_layouts: &HashMap<String, SgLayout>,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
offset_x: f64,
offset_y: f64,
node_global: &mut HashMap<String, (f64, f64, f64, f64)>,
sg_global_bounds: &mut HashMap<String, (f64, f64, f64, f64)>,
all_sg_full_sizes: &HashMap<String, (f64, f64)>,
margin2: f64,
) {
for v in top_g.nodes() {
if subgraph_ids.contains(&v) {
if let Some(n) = top_g.node_opt(&v) {
let cx = n.x.unwrap_or(0.0) + offset_x;
let cy = n.y.unwrap_or(0.0) + offset_y;
let (full_w, full_h) = all_sg_full_sizes
.get(&v)
.copied()
.unwrap_or((n.width + margin2, n.height + margin2));
sg_global_bounds.insert(v.clone(), (cx, cy, full_w, full_h));
if let Some(sg_layout) = sg_layouts.get(&v) {
let sg_ox = cx - full_w / 2.0;
let sg_oy = cy - full_h / 2.0;
composite_sg_layout(
&v,
sg_layout,
sg_layouts,
subgraph_ids,
diag,
sg_ox,
sg_oy,
node_global,
sg_global_bounds,
all_sg_full_sizes,
margin2,
&mut Vec::new(),
);
}
}
} else {
if let Some(n) = top_g.node_opt(&v) {
let cx = n.x.unwrap_or(0.0) + offset_x;
let cy = n.y.unwrap_or(0.0) + offset_y;
node_global.insert(v.clone(), (cx, cy, n.width, n.height));
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::only_used_in_recursion)]
fn composite_sg_layout(
_sg_id: &str,
sg_layout: &SgLayout,
all_sg_layouts: &HashMap<String, SgLayout>,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
sg_ox: f64, sg_oy: f64,
node_global: &mut HashMap<String, (f64, f64, f64, f64)>,
sg_global_bounds: &mut HashMap<String, (f64, f64, f64, f64)>,
all_sg_full_sizes: &HashMap<String, (f64, f64)>,
margin2: f64,
edge_global: &mut Vec<(String, String, Vec<Point>)>,
) {
for (id, &(cx, cy, w, h)) in &sg_layout.node_positions {
node_global.insert(id.clone(), (cx + sg_ox, cy + sg_oy, w, h));
}
for (from, to, pts) in &sg_layout.edges {
let global_pts: Vec<Point> = pts
.iter()
.map(|p| Point {
x: p.x + sg_ox,
y: p.y + sg_oy,
})
.collect();
if global_pts.len() >= 2 {
edge_global.push((from.clone(), to.clone(), global_pts));
}
}
for (child_sg_id, &(origin_x, origin_y)) in &sg_layout.child_sg_origins {
let (full_w, full_h) = all_sg_full_sizes
.get(child_sg_id)
.or_else(|| sg_layout.child_sg_full_sizes.get(child_sg_id))
.copied()
.unwrap_or_else(|| {
let (rw, rh) = sg_layout
.child_sg_sizes
.get(child_sg_id)
.copied()
.unwrap_or((100.0, 100.0));
(rw + margin2, rh + margin2)
});
let global_cx = origin_x + sg_ox + full_w / 2.0;
let global_cy = origin_y + sg_oy + full_h / 2.0;
sg_global_bounds.insert(child_sg_id.clone(), (global_cx, global_cy, full_w, full_h));
if let Some(child_layout) = all_sg_layouts.get(child_sg_id) {
let child_global_ox = global_cx - full_w / 2.0;
let child_global_oy = global_cy - full_h / 2.0;
composite_sg_layout(
child_sg_id,
child_layout,
all_sg_layouts,
subgraph_ids,
diag,
child_global_ox,
child_global_oy,
node_global,
sg_global_bounds,
all_sg_full_sizes,
margin2,
edge_global,
);
}
}
}
fn build_css(id: &str, vars: &ThemeVars) -> String {
let pf = vars.primary_color;
let ps = vars.primary_border;
let cf = vars.cluster_bg;
let cs = vars.cluster_border;
let ff = vars.font_family;
let mut c = String::new();
c.push_str(&format!(
"#{id}{{font-family:{ff};font-size:16px;fill:#333;}}"
));
c.push_str("@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}");
c.push_str("@keyframes dash{to{stroke-dashoffset:0;}}");
c.push_str(&format!("#{id} .edge-animation-slow{{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}}"));
c.push_str(&format!("#{id} .edge-animation-fast{{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}}"));
c.push_str(&format!("#{id} .error-icon{{fill:#552222;}}"));
c.push_str(&format!(
"#{id} .error-text{{fill:#552222;stroke:#552222;}}"
));
c.push_str(&format!(
"#{id} .edge-thickness-normal{{stroke-width:1px;}}"
));
c.push_str(&format!(
"#{id} .edge-thickness-thick{{stroke-width:3.5px;}}"
));
c.push_str(&format!("#{id} .edge-pattern-solid{{stroke-dasharray:0;}}"));
c.push_str(&format!(
"#{id} .edge-thickness-invisible{{stroke-width:0;fill:none;}}"
));
c.push_str(&format!(
"#{id} .edge-pattern-dashed{{stroke-dasharray:3;}}"
));
c.push_str(&format!(
"#{id} .edge-pattern-dotted{{stroke-dasharray:2;}}"
));
c.push_str(&format!("#{id} .marker{{fill:#333333;stroke:#333333;}}"));
c.push_str(&format!("#{id} .marker.cross{{stroke:#333333;}}"));
c.push_str(&format!("#{id} svg{{font-family:{ff};font-size:16px;}}"));
c.push_str(&format!("#{id} p{{margin:0;}}"));
c.push_str(&format!("#{id} .label{{font-family:{ff};color:#333;}}"));
c.push_str(&format!("#{id} .cluster-label text{{fill:#333;}}"));
c.push_str(&format!("#{id} .cluster-label span{{color:#333;}}"));
c.push_str(&format!(
"#{id} .cluster-label span p{{background-color:transparent;}}"
));
c.push_str(&format!(
"#{id} .label text,#{id} span{{fill:#333;color:#333;}}"
));
c.push_str(&format!("#{id} .node rect,#{id} .node circle,#{id} .node ellipse,#{id} .node polygon,#{id} .node path{{fill:{pf};stroke:{ps};stroke-width:1px;}}"));
c.push_str(&format!("#{id} .rough-node .label text,#{id} .node .label text,#{id} .image-shape .label,#{id} .icon-shape .label{{text-anchor:middle;}}"));
c.push_str(&format!(
"#{id} .node .katex path{{fill:#000;stroke:#000;stroke-width:1px;}}"
));
c.push_str(&format!("#{id} .rough-node .label,#{id} .node .label,#{id} .image-shape .label,#{id} .icon-shape .label{{text-align:center;}}"));
c.push_str(&format!("#{id} .node.clickable{{cursor:pointer;}}"));
c.push_str(&format!(
"#{id} .root .anchor path{{fill:#333333!important;stroke-width:0;stroke:#333333;}}"
));
c.push_str(&format!("#{id} .arrowheadPath{{fill:#333333;}}"));
c.push_str(&format!(
"#{id} .edgePath .path{{stroke:#333333;stroke-width:1px;}}"
));
c.push_str(&format!(
"#{id} .flowchart-link{{stroke:#333333;fill:none;}}"
));
c.push_str(&format!(
"#{id} .edgeLabel{{background-color:rgba(232,232,232, 0.8);text-align:center;}}"
));
c.push_str(&format!(
"#{id} .edgeLabel p{{background-color:rgba(232,232,232, 0.8);}}"
));
c.push_str(&format!("#{id} .edgeLabel rect{{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}}"));
c.push_str(&format!(
"#{id} .labelBkg{{background-color:rgba(232, 232, 232, 0.5);}}"
));
c.push_str(&format!(
"#{id} .cluster rect{{fill:{cf};stroke:{cs};stroke-width:1px;}}"
));
c.push_str(&format!("#{id} .cluster text{{fill:#333;}}"));
c.push_str(&format!("#{id} .cluster span{{color:#333;}}"));
c.push_str(&format!("#{id} div.mermaidTooltip{{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:{ff};font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}}"));
c.push_str(&format!(
"#{id} .flowchartTitleText{{text-anchor:middle;font-size:18px;fill:#333;}}"
));
c.push_str(&format!("#{id} rect.text{{fill:none;stroke-width:0;}}"));
c.push_str(&format!("#{id} .icon-shape,#{id} .image-shape{{background-color:rgba(232,232,232, 0.8);text-align:center;}}"));
c.push_str(&format!("#{id} .icon-shape p,#{id} .image-shape p{{background-color:rgba(232,232,232, 0.8);padding:2px;}}"));
c.push_str(&format!("#{id} .icon-shape .label rect,#{id} .image-shape .label rect{{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}}"));
c.push_str(&format!("#{id} .label-icon{{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}}"));
c.push_str(&format!(
"#{id} .node .label-icon path{{fill:currentColor;stroke:revert;stroke-width:revert;}}"
));
c.push_str(&format!("#{id} .node .neo-node{{stroke:{ps};}}"));
c.push_str(&format!("#{id} [data-look=\"neo\"].node rect,#{id} [data-look=\"neo\"].cluster rect,#{id} [data-look=\"neo\"].node polygon{{stroke:{ps};filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}}"));
c.push_str(&format!(
"#{id} [data-look=\"neo\"].node path{{stroke:{ps};stroke-width:1px;}}"
));
c.push_str(&format!("#{id} [data-look=\"neo\"].node .outer-path{{filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}}"));
c.push_str(&format!(
"#{id} [data-look=\"neo\"].node .neo-line path{{stroke:{ps};filter:none;}}"
));
c.push_str(&format!("#{id} [data-look=\"neo\"].node circle{{stroke:{ps};filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}}"));
c.push_str(&format!(
"#{id} [data-look=\"neo\"].node circle .state-start{{fill:#000000;}}"
));
c.push_str(&format!("#{id} [data-look=\"neo\"].icon-shape .icon{{fill:{ps};filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}}"));
c.push_str(&format!("#{id} [data-look=\"neo\"].icon-shape .icon-neo path{{stroke:{ps};filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}}"));
c.push_str(&format!("#{id} :root{{--mermaid-font-family:{ff};}}"));
c
}
fn shape_intersect_type(shape: &NodeShape) -> Option<&'static str> {
match shape {
NodeShape::Diamond | NodeShape::Hexagon => Some("diamond"),
NodeShape::Circle => Some("circle"),
_ => None,
}
}
fn node_size(label: &str, shape: &NodeShape) -> (f64, f64) {
let (raw_tw, _) = measure(label, FONT_SIZE);
let tw = raw_tw * TEXT_SCALE;
match shape {
NodeShape::Rectangle | NodeShape::Default => ((tw + H_PAD * 2.0).max(50.0), RECT_H),
NodeShape::Subroutine => {
((tw + SUBROUTINE_H_PAD).max(50.0), COMPACT_H)
}
NodeShape::RoundedRect => ((tw + SMALL_PAD * 2.0).max(40.0), RECT_H),
NodeShape::Cylinder => {
let w = (tw + CYLINDER_H_PAD).max(40.0);
let ry = (w / 2.0 * CYLINDER_RY_FACTOR).max(CYLINDER_MIN_RY);
let h = CYLINDER_BODY_H + 2.0 * ry;
(w, h)
}
NodeShape::Stadium => ((tw + SMALL_PAD * 2.0).max(40.0), COMPACT_H),
NodeShape::Diamond | NodeShape::Hexagon => {
let dim = (tw + DIAMOND_PAD * 2.0).max(60.0);
(dim, dim)
}
NodeShape::Circle => {
let r = (tw / 2.0 + CIRCLE_LABEL_PAD).max(CIRCLE_MIN_RADIUS);
(r * 2.0, r * 2.0)
}
NodeShape::Asymmetric => {
(
(tw + ASYMMETRIC_BASE_PAD + ASYMMETRIC_NOTCH_DEPTH).max(40.0),
COMPACT_H,
)
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_node(
node: &super::parser::FlowNode,
cx: f64,
cy: f64,
w: f64,
h: f64,
vars: &ThemeVars,
style: Option<&NodeStyle>,
dom_id: &str,
use_foreign_object: bool,
) -> String {
let (raw_tw, _) = measure(&node.label, FONT_SIZE);
let tw = raw_tw * TEXT_SCALE;
let ff = vars.font_family;
let (inline_style, label_color_style, div_color_style) = if let Some(s) = style {
let mut shape_parts = Vec::new();
if let Some(f) = s.fill.as_deref() {
shape_parts.push(format!("fill:{} !important", f));
}
if let Some(st) = s.stroke.as_deref() {
shape_parts.push(format!("stroke:{} !important", st));
}
if let Some(sw) = s.stroke_width.as_deref() {
shape_parts.push(format!("stroke-width:{} !important", sw));
}
let lc = s
.color
.as_deref()
.map(|c| format!("color:{} !important", c))
.unwrap_or_default();
let dc = s
.color
.as_deref()
.map(|c| format!("color: {} !important; ", c))
.unwrap_or_default();
(shape_parts.join(";"), lc, dc)
} else {
(String::new(), String::new(), String::new())
};
let mut s = String::new();
s.push_str(&templates::node_group(dom_id, &fmt(cx), &fmt(cy)));
match node.shape {
NodeShape::Rectangle | NodeShape::Default => {
s.push_str(&templates::node_rect(
&fmt(-w / 2.0),
&fmt(w),
&inline_style,
));
}
NodeShape::RoundedRect => {
s.push_str(&templates::node_rounded_rect(
&fmt(-w / 2.0),
&fmt(w),
&inline_style,
));
}
NodeShape::Stadium => {
let half_h = COMPACT_H / 2.0;
let rx = half_h;
s.push_str(&templates::node_stadium_rect(
&fmt(rx),
&fmt(-w / 2.0),
&fmt(-half_h),
&fmt(w),
&fmt(COMPACT_H),
&inline_style,
));
}
NodeShape::Diamond => {
let hw = w / 2.0;
let hh = h / 2.0;
s.push_str(&templates::node_diamond(
&fmt(hw),
&fmt(w),
&fmt(-hh),
&fmt(-h),
&fmt(-hw),
&fmt(hh),
&inline_style,
));
}
NodeShape::Circle => {
s.push_str(&templates::node_circle(&fmt(w / 2.0), &inline_style));
}
NodeShape::Asymmetric => {
let hw = w / 2.0;
let hh = h / 2.0;
let notch = ASYMMETRIC_NOTCH_DEPTH;
let pts = format!(
"{},{} {},{} {},{} {},{} {},{}",
fmt(-hw),
fmt(-hh), fmt(hw),
fmt(-hh), fmt(hw),
fmt(hh), fmt(-hw),
fmt(hh), fmt(-hw + notch),
fmt(0.0), );
s.push_str(&templates::node_asymmetric(&pts, &inline_style));
}
NodeShape::Cylinder => {
let hw = w / 2.0;
let hh = h / 2.0;
let ry = (hw * CYLINDER_RY_FACTOR).max(CYLINDER_MIN_RY);
let body_half = hh - ry; let stroke = style
.and_then(|s| s.stroke.as_deref())
.unwrap_or(vars.primary_border);
let d = format!(
"M {},{} a {},{} 0,0,0 {},0 a {},{} 0,0,0 {},0 l0,{} a {},{} 0,0,0 {},0 l0,{}",
fmt(-hw),
fmt(-body_half),
fmt(hw),
fmt(ry),
fmt(w),
fmt(hw),
fmt(ry),
fmt(-w),
fmt(2.0 * body_half),
fmt(hw),
fmt(ry),
fmt(w),
fmt(-2.0 * body_half),
);
s.push_str(&templates::node_cylinder_body(&d, &inline_style));
let top_d = format!(
"M {},{} a {},{} 0,0,0 {},0",
fmt(-hw),
fmt(-body_half),
fmt(hw),
fmt(ry),
fmt(w),
);
s.push_str(&templates::node_cylinder_top(&top_d, stroke));
}
NodeShape::Subroutine => {
let half_h = COMPACT_H / 2.0;
let stroke = style
.and_then(|s| s.stroke.as_deref())
.unwrap_or(vars.primary_border);
s.push_str(&templates::node_subroutine_rect(
&fmt(-w / 2.0),
&fmt(-half_h),
&fmt(w),
&fmt(COMPACT_H),
&inline_style,
));
s.push_str(&templates::node_subroutine_line(
&fmt(-w / 2.0 + SUBROUTINE_LINE_INSET),
&fmt(-half_h),
&fmt(half_h),
stroke,
));
s.push_str(&templates::node_subroutine_line(
&fmt(w / 2.0 - SUBROUTINE_LINE_INSET),
&fmt(-half_h),
&fmt(half_h),
stroke,
));
}
NodeShape::Hexagon => {
let hw = w / 2.0;
let hh = h / 2.0;
let indent = hh * 0.5;
let pts = format!(
"{},{} {},{} {},{} {},{} {},{} {},{}",
fmt(-hw + indent),
fmt(-hh),
fmt(hw - indent),
fmt(-hh),
fmt(hw),
0.0,
fmt(hw - indent),
fmt(hh),
fmt(-hw + indent),
fmt(hh),
fmt(-hw),
0.0,
);
s.push_str(&templates::node_hexagon(&pts, &inline_style));
}
}
let span_color = if !div_color_style.is_empty() {
format!(
" style=\"{}\"",
div_color_style.trim_end_matches(' ').trim_end_matches(';')
)
} else {
String::new()
};
if use_foreign_object {
let label_ty = if node.shape == NodeShape::Cylinder {
CYLINDER_LABEL_Y_OFFSET
} else {
LABEL_Y_OFFSET
};
s.push_str(&templates::node_label_fo(
&label_color_style,
&fmt(-tw / 2.0),
label_ty,
&fmt(tw),
LABEL_FO_HEIGHT,
&div_color_style,
&span_color,
&esc(&node.label),
));
} else {
let text_fill = if !label_color_style.is_empty() {
style
.and_then(|s| s.color.as_deref())
.unwrap_or(vars.primary_text)
} else {
vars.primary_text
};
s.push_str(&templates::node_label_text(
&label_color_style,
TEXT_LABEL_Y,
ff,
FONT_SIZE,
text_fill,
&esc(&node.label),
));
}
s.push_str("</g>");
s
}
fn collect_descendants(
sg_id: &str,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
) -> HashSet<String> {
let mut result = HashSet::new();
if let Some(sg) = diag.subgraphs.iter().find(|sg| sg.id == sg_id) {
for member in &sg.members {
result.insert(member.clone());
if subgraph_ids.contains(member) {
let inner = collect_descendants(member, subgraph_ids, diag);
result.extend(inner);
}
}
}
result
}
fn collect_leaf_ids(
sg_id: &str,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
) -> Vec<String> {
let mut result = Vec::new();
if let Some(sg) = diag.subgraphs.iter().find(|sg| sg.id == sg_id) {
for member in &sg.members {
if subgraph_ids.contains(member) {
result.extend(collect_leaf_ids(member, subgraph_ids, diag));
} else {
result.push(member.clone());
}
}
}
result
}
fn is_sg_at_level(
sg_id: &str,
sg_parent: &HashMap<String, String>,
self_cluster: Option<&Subgraph>,
) -> bool {
match self_cluster {
None => sg_parent.get(sg_id).is_none(),
Some(sc) => sg_parent.get(sg_id).map(|p| p == &sc.id).unwrap_or(false),
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_edge_endpoint<'a>(
id: &str,
subgraph_ids: &HashSet<String>,
diag: &'a FlowchartDiagram,
g: &Graph,
context_sgs: &[&'a Subgraph],
node_subgraph: &HashMap<String, String>,
sg_parent: &HashMap<String, String>,
sg_external: &HashSet<String>,
self_cluster: Option<&'a Subgraph>,
) -> Option<String> {
if subgraph_ids.contains(id) {
if sg_external.contains(id) {
let leaf = resolve_to_leaf(id, subgraph_ids, diag)?;
if g.has_node(&leaf) {
return Some(leaf);
}
return None;
}
if is_sg_at_level(id, sg_parent, self_cluster) && g.has_node(id) {
return Some(id.to_string());
}
None
} else {
if node_at_level(
id,
context_sgs,
node_subgraph,
sg_parent,
sg_external,
self_cluster,
) {
Some(id.to_string())
} else {
None
}
}
}
fn resolve_to_leaf(
id: &str,
subgraph_ids: &HashSet<String>,
diag: &FlowchartDiagram,
) -> Option<String> {
if !subgraph_ids.contains(id) {
return Some(id.to_string());
}
if let Some(sg) = diag.subgraphs.iter().find(|sg| sg.id == id) {
for member in &sg.members {
if let Some(leaf) = resolve_to_leaf(member, subgraph_ids, diag) {
return Some(leaf);
}
}
}
None
}
fn node_at_level(
node_id: &str,
context_sgs: &[&Subgraph],
node_subgraph: &HashMap<String, String>,
sg_parent: &HashMap<String, String>,
sg_external: &HashSet<String>,
self_cluster: Option<&Subgraph>,
) -> bool {
match self_cluster {
None => {
match node_subgraph.get(node_id) {
None => {
context_sgs.iter().all(|sg| !sg_parent.contains_key(&sg.id))
}
Some(direct_parent) => {
let in_context = context_sgs.iter().any(|sg| sg.id == *direct_parent);
if in_context {
sg_external.contains(direct_parent)
} else {
let mut cur = direct_parent.clone();
loop {
match sg_parent.get(&cur) {
None => return false, Some(anc) => {
if context_sgs.iter().any(|sg| sg.id == *anc) {
return sg_external.contains(anc.as_str());
}
cur = anc.clone();
}
}
}
}
}
}
}
Some(sc) => {
match node_subgraph.get(node_id) {
Some(direct_parent) if direct_parent == &sc.id => {
true
}
Some(direct_parent)
if context_sgs.iter().any(|sg| sg.id == *direct_parent) =>
{
sg_external.contains(direct_parent)
}
Some(_) => false, None => false, }
}
}
}
#[allow(dead_code)]
fn compute_subgraph_bbox(leaves: &[String], g: &Graph) -> Option<(f64, f64, f64, f64)> {
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for member in leaves {
if let Some(n) = g.node_opt(member) {
let x = n.x.unwrap_or(0.0);
let y = n.y.unwrap_or(0.0);
let hw = n.width / 2.0;
let hh = n.height / 2.0;
min_x = min_x.min(x - hw);
max_x = max_x.max(x + hw);
min_y = min_y.min(y - hh);
max_y = max_y.max(y + hh);
}
}
if min_x == f64::MAX {
return None;
}
Some((
min_x - SG_PAD_H,
min_y - SG_PAD_T,
max_x - min_x + SG_PAD_H * 2.0,
max_y - min_y + SG_PAD_T + SG_PAD_B,
))
}
fn trim_path_end(pts: &[Point], amount: f64) -> Vec<Point> {
if amount <= 0.0 || pts.len() < 2 {
return pts.to_vec();
}
let mut r = pts.to_vec();
let n = r.len();
let last = r[n - 1].clone();
let prev = r[n - 2].clone();
let dx = last.x - prev.x;
let dy = last.y - prev.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= amount {
r.truncate(n - 1);
} else {
let frac = (len - amount) / len;
r[n - 1] = Point {
x: prev.x + dx * frac,
y: prev.y + dy * frac,
};
}
r
}
#[allow(dead_code)]
fn trim_path_start(pts: &[Point], amount: f64) -> Vec<Point> {
if amount <= 0.0 || pts.len() < 2 {
return pts.to_vec();
}
let mut r = pts.to_vec();
let first = r[0].clone();
let next = r[1].clone();
let dx = next.x - first.x;
let dy = next.y - first.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= amount {
r.remove(0);
} else {
let frac = amount / len;
r[0] = Point {
x: first.x + dx * frac,
y: first.y + dy * frac,
};
}
r
}
fn edge_path(pts: &[Point]) -> String {
let pairs: Vec<(f64, f64)> = pts.iter().map(|p| (p.x, p.y)).collect();
crate::svg::curve_basis_path(&pairs)
}
fn midpoint(pts: &[Point]) -> (f64, f64) {
if pts.is_empty() {
return (0.0, 0.0);
}
let mid = pts.len() / 2;
(pts[mid].x, pts[mid].y)
}
fn fmt(v: f64) -> String {
let s = format!("{:.7}", v);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn pt_in_rect(p: &Point, cx: f64, cy: f64, w: f64, h: f64) -> bool {
p.x >= cx - w / 2.0 - 1.0
&& p.x <= cx + w / 2.0 + 1.0
&& p.y >= cy - h / 2.0 - 1.0
&& p.y <= cy + h / 2.0 + 1.0
}
fn rect_exit(p0: &Point, p1: &Point, cx: f64, cy: f64, w: f64, h: f64) -> Option<Point> {
let left = cx - w / 2.0;
let right = cx + w / 2.0;
let top = cy - h / 2.0;
let bot = cy + h / 2.0;
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let mut best_t = f64::INFINITY;
for &(val, is_y) in &[(left, false), (right, false), (top, true), (bot, true)] {
let (denom, numer, pmin, pmax) = if is_y {
(dy, val - p0.y, left, right)
} else {
(dx, val - p0.x, top, bot)
};
if denom.abs() < 1e-9 {
continue;
}
let t = numer / denom;
if !(1e-6..=1.0 + 1e-6).contains(&t) {
continue;
}
let perp = if is_y { p0.x + dx * t } else { p0.y + dy * t };
if perp < pmin - 1.0 || perp > pmax + 1.0 {
continue;
}
if t < best_t {
best_t = t;
}
}
if best_t.is_finite() {
Some(Point {
x: p0.x + dx * best_t,
y: p0.y + dy * best_t,
})
} else {
None
}
}
fn clip_path_from_cluster(
pts: &[Point],
cx: f64,
cy: f64,
w: f64,
h: f64,
from_start: bool,
) -> Vec<Point> {
if pts.len() < 2 {
return pts.to_vec();
}
if from_start {
let mut last_in = 0usize;
for (i, pt) in pts.iter().enumerate().take(pts.len() - 1) {
if pt_in_rect(pt, cx, cy, w, h) {
last_in = i;
} else {
break;
}
}
if last_in == 0 && !pt_in_rect(&pts[0], cx, cy, w, h) {
return pts.to_vec(); }
if let Some(exit) = rect_exit(&pts[last_in], &pts[last_in + 1], cx, cy, w, h) {
let mut result = vec![exit];
result.extend_from_slice(&pts[last_in + 1..]);
return result;
}
pts.to_vec()
} else {
let n = pts.len();
let mut last_in = n - 1;
for i in (1..n).rev() {
if pt_in_rect(&pts[i], cx, cy, w, h) {
last_in = i;
} else {
break;
}
}
if last_in == n - 1 && !pt_in_rect(&pts[n - 1], cx, cy, w, h) {
return pts.to_vec();
}
if let Some(entry) = rect_exit(&pts[last_in], &pts[last_in - 1], cx, cy, w, h) {
let mut result = pts[..last_in].to_vec();
result.push(entry);
return result;
}
pts.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::super::parser;
use super::*;
const FLOWCHART_BASIC: &str = "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
#[test]
fn basic_render_produces_svg() {
let diag = parser::parse(FLOWCHART_BASIC).diagram;
let svg = render(&diag, Theme::Default, false);
assert!(svg.contains("<svg"), "missing <svg tag");
assert!(svg.contains("Christmas"), "missing node label");
assert!(svg.contains("Go shopping"), "missing node label");
}
#[test]
fn dark_theme() {
let diag = parser::parse(FLOWCHART_BASIC).diagram;
let svg = render(&diag, Theme::Dark, false);
assert!(svg.contains("<svg"), "missing <svg tag");
}
#[test]
#[ignore = "platform-specific float precision — run locally"]
fn snapshot_default_theme() {
let diag = parser::parse(FLOWCHART_BASIC).diagram;
let svg = render(&diag, crate::theme::Theme::Default, false);
insta::assert_snapshot!(svg);
}
}