use crate::preview::mermaid::flowchart::{Shape, Stroke};
use crate::preview::mermaid::gitgraph::{CommitKind, Direction, GitGraph};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::chart::label_node;
use super::{
normalise, shapes, svg, Diagram, Glyph, Label, PlacedEdge, PlacedNode, RenderError, Size,
Theme, Tip,
};
pub const COMMIT_STEP: f64 = 46.0;
pub const LANE_STEP: f64 = 44.0;
pub const NAME_GAP: f64 = 10.0;
pub const CAPTION_GAP: f64 = 6.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let graph = crate::preview::mermaid::gitgraph::parse(code)?;
let diagram = lay_out(&graph)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(graph: &GitGraph) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if graph.commits.is_empty() {
return Err(RenderError::NothingToDraw);
}
let lanes: Vec<&str> = graph.lanes().iter().map(|b| b.name.as_str()).collect();
let lane_of = |branch: &str| lanes.iter().position(|b| *b == branch).unwrap_or(0);
let names: Vec<Label> = lanes.iter().map(|b| Label::measure(b)).collect();
let widest = names.iter().map(|l| l.width).fold(0.0_f64, f64::max);
let x0 = widest + NAME_GAP;
let lane_step = match graph.direction {
Direction::LeftToRight => LANE_STEP,
_ => LANE_STEP.max(widest + NAME_GAP),
};
let along = move |seq: usize| x0 + seq as f64 * COMMIT_STEP + shapes::COMMIT_RADIUS;
let across = move |lane: usize| lane as f64 * lane_step + shapes::COMMIT_RADIUS;
let place = |seq: usize, lane: usize| -> Point {
match graph.direction {
Direction::LeftToRight => Point::new(along(seq), across(lane)),
Direction::TopToBottom => Point::new(across(lane), along(seq)),
Direction::BottomToTop => Point::new(across(lane), -along(seq)),
}
};
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut edges: Vec<PlacedEdge> = Vec::new();
let d = shapes::COMMIT_RADIUS * 2.0;
for (i, name) in names.iter().enumerate() {
let at = match graph.direction {
Direction::LeftToRight => Point::new(widest - name.width / 2.0, across(i)),
_ => Point::new(across(i), -NAME_GAP - name.height / 2.0),
};
if let Some(n) = label_node(format!("branch#{}", lanes[i]), name.clone(), at, None) {
nodes.push(n);
}
}
for commit in &graph.commits {
let lane = lane_of(&commit.branch);
let center = place(commit.seq, lane);
let drawn = commit.custom_type.unwrap_or(commit.kind);
let (glyph, size) = match drawn {
CommitKind::Reverse => (Glyph::Reverted, Size::new(d, d)),
CommitKind::Highlight => (Glyph::Flow(Shape::Rect), Size::new(d, d)),
CommitKind::Merge => (Glyph::Flow(Shape::DoubleCircle), Size::new(d, d)),
CommitKind::CherryPick | CommitKind::Normal => {
(Glyph::Flow(Shape::Circle), Size::new(d, d))
}
};
nodes.push(PlacedNode {
id: commit.id.clone(),
shape: glyph,
center: center.clone(),
size,
label: Label::measure(""),
panel: None,
series: Some(lane),
mark: None,
});
let caption = if !commit.tags.is_empty() {
commit.tags.join(", ")
} else if commit.explicit_id {
commit.id.clone()
} else {
String::new()
};
let label = Label::measure(&caption);
let at = Point::new(
center.x,
center.y + shapes::COMMIT_RADIUS + CAPTION_GAP + label.height / 2.0,
);
if let Some(n) = label_node(format!("{}#caption", commit.id), label, at, None) {
nodes.push(n);
}
}
for commit in &graph.commits {
let lane = lane_of(&commit.branch);
let head = place(commit.seq, lane);
for parent_id in &commit.parents {
let Some(parent) = graph.commit(parent_id) else {
continue;
};
let plane = lane_of(&parent.branch);
let tail = place(parent.seq, plane);
let mut points = vec![tail.clone()];
if plane != lane {
points.push(match graph.direction {
Direction::LeftToRight => Point::new(head.x, tail.y),
_ => Point::new(tail.x, head.y),
});
}
points.push(head.clone());
edges.push(PlacedEdge {
from: parent.id.clone(),
to: commit.id.clone(),
points,
tip_start: Tip::None,
tip_end: Tip::None,
stroke: Stroke::Normal,
label: None,
start_label: None,
end_label: None,
badge: None,
series: Some(plane),
straight: true,
});
}
}
let mut diagram = Diagram {
nodes,
edges,
..Diagram::default()
};
band::add_title(&mut diagram, graph.preamble.title.as_deref());
normalise(&mut diagram);
Ok(diagram)
}