use super::constants::*;
use super::parser::TreeViewDiagram;
use super::templates::{self, build_css, esc};
use crate::text_browser_metrics::measure_browser;
use crate::theme::Theme;
struct RenderedNode {
label: String,
text_x: f64,
text_y: f64,
last_child_idx: Option<usize>,
}
fn flatten(
label: &str,
children: &[super::parser::TreeNode],
depth: usize,
row: &mut usize,
out: &mut Vec<RenderedNode>,
) {
let text_x = depth as f64 * INDENT_STEP + LEFT_PAD;
let text_y = (*row as f64 + 0.5) * ROW_HEIGHT;
let my_idx = out.len();
out.push(RenderedNode {
label: label.to_string(),
text_x,
text_y,
last_child_idx: None, });
*row += 1;
if !children.is_empty() {
let mut last_direct_child_idx = 0usize;
for child in children {
last_direct_child_idx = out.len();
flatten(&child.label, &child.children, depth + 1, row, out);
}
out[my_idx].last_child_idx = Some(last_direct_child_idx);
}
}
pub fn render(diag: &TreeViewDiagram, _theme: Theme) -> String {
let root_label = "/";
let root_children = &diag.roots;
let mut row = 0usize;
let mut nodes: Vec<RenderedNode> = Vec::new();
let root_text_x = LEFT_PAD;
let root_text_y = (row as f64 + 0.5) * ROW_HEIGHT;
let root_my_idx = nodes.len();
nodes.push(RenderedNode {
label: root_label.to_string(),
text_x: root_text_x,
text_y: root_text_y,
last_child_idx: None,
});
row += 1;
let mut last_direct_child_of_root = 0usize;
for child in root_children {
last_direct_child_of_root = nodes.len();
flatten(&child.label, &child.children, 1, &mut row, &mut nodes);
}
if !root_children.is_empty() {
nodes[root_my_idx].last_child_idx = Some(last_direct_child_of_root);
}
let total_rows = row;
let vb_h = total_rows as f64 * ROW_HEIGHT;
let max_right = nodes
.iter()
.map(|n| {
let (tw, _) = measure_browser(&n.label, FONT_SIZE);
n.text_x + tw + TRAILING_ADVANCE
})
.fold(0.0_f64, f64::max);
let vb_w = max_right + VIEWBOX_RIGHT_PAD - VIEWBOX_X;
let svg_id = "mermaid-svg-99";
let css = build_css(svg_id);
let mut out = String::new();
out.push_str(&templates::svg_root(
svg_id,
vb_w + VIEWBOX_X, VIEWBOX_X,
VIEWBOX_Y,
vb_w,
vb_h,
));
out.push_str(&format!("<style>{css}</style>"));
out.push_str("<g></g>");
out.push_str(r#"<g class="tree-view">"#);
for node in &nodes {
out.push_str(&templates::node_text(
node.text_x,
node.text_y,
&esc(&node.label),
));
let h_x1 = node.text_x - INDENT_STEP;
let h_x2 = node.text_x - H_LINE_GAP;
out.push_str(&templates::h_line(h_x1, node.text_y, h_x2, node.text_y));
}
for node in nodes.iter().rev() {
if let Some(last_child_idx) = node.last_child_idx {
let v_x = node.text_x;
let v_y1 = node.text_y + ROW_HEIGHT / 2.0;
let v_y2 = nodes[last_child_idx].text_y + V_LINE_BOTTOM_TWEAK;
out.push_str(&templates::v_line(v_x, v_y1, v_x, v_y2));
}
}
out.push_str("</g>");
out.push_str("</svg>");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagrams::treeview::parser;
#[test]
fn basic_render_produces_svg() {
let input = concat!(
"treeView-beta\n",
" \"docs\"\n",
" \"build\"\n",
" \"make.bat\"\n",
);
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains("<svg"), "no <svg tag");
assert!(svg.contains("docs"));
assert!(svg.contains("build"));
assert!(svg.contains("tree-view"));
assert!(svg.contains("treeView-node-label"));
assert!(svg.contains("treeView-node-line"));
}
#[test]
fn synthetic_root_slash_is_present() {
let input = "treeView-beta\n \"docs\"\n";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains('>'), "svg should have content");
assert!(
svg.contains(">/</text>") || svg.contains(">/</"),
"slash root missing"
);
}
#[test]
fn viewbox_height_matches_row_count() {
let input = concat!("treeView-beta\n", " \"docs\"\n", " \"build\"\n",);
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
let expected_h = 3.0 * ROW_HEIGHT;
assert!(
svg.contains(&format!("{expected_h}")),
"expected height {expected_h} not found in: {svg}"
);
}
#[test]
fn empty_diagram_has_only_root() {
let input = "treeView-beta\n";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains("<svg"));
let expected_h = ROW_HEIGHT;
assert!(
svg.contains(&format!("{expected_h}")),
"expected height {expected_h} not found"
);
}
}