#![allow(dead_code)]
pub mod architecture;
pub mod band;
pub mod block;
pub mod c4;
pub mod chart;
pub mod class;
pub mod clusters;
pub mod edges;
pub mod er;
pub mod gantt;
pub mod gitgraph;
pub mod journey;
pub mod kanban;
pub mod labels;
pub mod mindmap;
pub mod panel;
pub mod requirement;
pub mod sequence;
pub mod shapes;
pub mod state;
pub mod svg;
pub mod theme;
pub mod timeline;
pub mod zenuml;
#[cfg(test)]
mod class_tests;
#[cfg(test)]
mod decoration_tests;
#[cfg(test)]
mod er_tests;
#[cfg(test)]
mod kinds_tests;
#[cfg(test)]
mod sequence_tests;
#[cfg(test)]
mod state_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod text_placement_tests;
use std::collections::{HashMap, HashSet};
use std::fmt;
use crate::preview::mermaid::flowchart::{self, Direction, Flowchart, ParseError, Stroke};
use crate::preview::mermaid::layout::{
graph::{Graph, GraphOptions},
layout, EdgeLabel, LayoutOptions, NodeLabel, Point, RankDir,
};
use crate::preview::mermaid::text_metrics;
pub use edges::Tip;
pub use labels::Label;
pub use panel::Panel;
#[allow(unused_imports)]
pub use shapes::{Glyph, Mark, Size};
pub use theme::Theme;
pub const MARGIN: f64 = 8.0;
pub const NODE_SEP: f64 = 50.0;
pub const RANK_SEP: f64 = 50.0;
pub const EDGE_SEP: f64 = 20.0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
Parse(ParseError),
StateParse(crate::preview::mermaid::state::ParseError),
ClassParse(crate::preview::mermaid::class::ParseError),
ErParse(crate::preview::mermaid::er::ParseError),
SequenceParse(crate::preview::mermaid::sequence::ParseError),
ChartParse(crate::preview::mermaid::chart::ParseError),
ChartHasNoExtent {
what: &'static str,
},
NoFonts,
NothingToDraw,
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RenderError::Parse(e) => write!(f, "{e}"),
RenderError::StateParse(e) => write!(f, "{e}"),
RenderError::ClassParse(e) => write!(f, "{e}"),
RenderError::ErParse(e) => write!(f, "{e}"),
RenderError::SequenceParse(e) => write!(f, "{e}"),
RenderError::ChartParse(e) => write!(f, "{e}"),
RenderError::ChartHasNoExtent { what } => write!(f, "{what}"),
RenderError::NoFonts => write!(f, "no sans-serif font available for diagram labels"),
RenderError::NothingToDraw => write!(f, "flowchart has nothing to draw"),
}
}
}
impl std::error::Error for RenderError {}
impl From<ParseError> for RenderError {
fn from(e: ParseError) -> RenderError {
RenderError::Parse(e)
}
}
impl From<crate::preview::mermaid::state::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::state::ParseError) -> RenderError {
RenderError::StateParse(e)
}
}
impl From<crate::preview::mermaid::class::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::class::ParseError) -> RenderError {
RenderError::ClassParse(e)
}
}
impl From<crate::preview::mermaid::er::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::er::ParseError) -> RenderError {
RenderError::ErParse(e)
}
}
impl From<crate::preview::mermaid::sequence::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::sequence::ParseError) -> RenderError {
RenderError::SequenceParse(e)
}
}
impl From<crate::preview::mermaid::chart::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::chart::ParseError) -> RenderError {
RenderError::ChartParse(e)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedNode {
pub id: String,
pub shape: Glyph,
pub center: Point,
pub size: Size,
pub label: Label,
pub panel: Option<Panel>,
pub series: Option<usize>,
pub mark: Option<shapes::Mark>,
}
impl PlacedNode {
pub fn bounds(&self) -> (f64, f64, f64, f64) {
(
self.center.x - self.size.w / 2.0,
self.center.y - self.size.h / 2.0,
self.center.x + self.size.w / 2.0,
self.center.y + self.size.h / 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedEdgeLabel {
pub center: Point,
pub size: Size,
pub label: Label,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedEdge {
pub from: String,
pub to: String,
pub points: Vec<Point>,
pub tip_start: Tip,
pub tip_end: Tip,
pub stroke: Stroke,
pub label: Option<PlacedEdgeLabel>,
pub start_label: Option<PlacedEdgeLabel>,
pub end_label: Option<PlacedEdgeLabel>,
pub badge: Option<PlacedEdgeLabel>,
pub series: Option<usize>,
pub straight: bool,
pub overlay: bool,
}
impl PlacedEdge {
pub fn drawn_points(&self) -> Vec<Point> {
if self.series.is_some() || self.straight {
return self.points.clone();
}
edges::fix_corners(&self.points)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedCluster {
pub id: String,
pub title: Label,
pub center: Point,
pub size: Size,
pub parent: Option<String>,
pub depth: usize,
pub dashed: bool,
pub filled: bool,
pub sections: Vec<ClusterSection>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClusterSection {
pub y: f64,
pub title: Label,
}
impl PlacedCluster {
pub fn bounds(&self) -> (f64, f64, f64, f64) {
(
self.center.x - self.size.w / 2.0,
self.center.y - self.size.h / 2.0,
self.center.x + self.size.w / 2.0,
self.center.y + self.size.h / 2.0,
)
}
pub fn title_center(&self) -> Point {
let (_, top, _, _) = self.bounds();
Point::new(
self.center.x,
top + clusters::TITLE_PAD_Y + self.title.height / 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Diagram {
pub width: f64,
pub height: f64,
pub nodes: Vec<PlacedNode>,
pub edges: Vec<PlacedEdge>,
pub clusters: Vec<PlacedCluster>,
pub lifelines: Vec<PlacedLifeline>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedLifeline {
pub id: String,
pub x: f64,
pub top: f64,
pub bottom: f64,
pub destroyed: bool,
pub activations: Vec<PlacedActivation>,
}
impl PlacedLifeline {
pub fn activation_bounds(&self, i: usize) -> Option<(f64, f64, f64, f64)> {
let a = self.activations.get(i)?;
let left = self.x - ACTIVATION_WIDTH / 2.0 + a.depth as f64 * ACTIVATION_WIDTH;
Some((left, a.top, left + ACTIVATION_WIDTH, a.bottom))
}
pub fn extent(&self) -> (f64, f64) {
let mut left = self.x;
let mut right = self.x;
for i in 0..self.activations.len() {
if let Some((l, _, r, _)) = self.activation_bounds(i) {
left = left.min(l);
right = right.max(r);
}
}
(left, right)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedActivation {
pub depth: usize,
pub top: f64,
pub bottom: f64,
}
pub const ACTIVATION_WIDTH: f64 = 10.0;
impl Diagram {
pub fn node(&self, id: &str) -> Option<&PlacedNode> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn cluster(&self, id: &str) -> Option<&PlacedCluster> {
self.clusters.iter().find(|c| c.id == id)
}
}
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let chart = flowchart::parse(code)?;
let diagram = lay_out(&chart)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(chart: &Flowchart) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if chart.nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
lay_out_spec(&spec_of(chart))
}
fn spec_of(chart: &Flowchart) -> GraphSpec {
let nodes = chart
.nodes
.iter()
.map(|node| {
let label = Label::measure(&node.label);
let glyph = Glyph::Flow(node.shape);
let size = shapes::size(glyph, Size::new(label.width, label.height));
SpecNode {
id: node.id.clone(),
glyph,
label,
size,
panel: None,
}
})
.collect();
let blocks = chart
.subgraphs
.iter()
.map(|s| SpecBlock {
id: s.id.clone(),
title: s.title.clone(),
members: s.members.clone(),
dashed: false,
})
.collect();
let edges = chart
.edges
.iter()
.map(|edge| SpecEdge {
id: edge.id.clone(),
from: edge.from.clone(),
to: edge.to.clone(),
label: edge
.label
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
tip_start: edges::Tip::of_arrow(edge.arrow).0,
tip_end: edges::Tip::of_arrow(edge.arrow).1,
stroke: edge.stroke,
minlen: edge.length,
start_label: None,
end_label: None,
})
.collect();
GraphSpec {
direction: chart.direction,
nodes,
edges,
blocks,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpecNode {
pub id: String,
pub glyph: Glyph,
pub label: Label,
pub size: Size,
pub panel: Option<Panel>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpecEdge {
pub id: String,
pub from: String,
pub to: String,
pub label: Option<Label>,
pub tip_start: Tip,
pub tip_end: Tip,
pub stroke: Stroke,
pub minlen: usize,
pub start_label: Option<Label>,
pub end_label: Option<Label>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpecBlock {
pub id: String,
pub title: String,
pub members: Vec<String>,
pub dashed: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GraphSpec {
pub direction: Direction,
pub nodes: Vec<SpecNode>,
pub edges: Vec<SpecEdge>,
pub blocks: Vec<SpecBlock>,
}
pub fn lay_out_spec(spec: &GraphSpec) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if spec.nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
let node_ids: HashSet<&str> = spec.nodes.iter().map(|n| n.id.as_str()).collect();
let tree = clusters::Tree::from_blocks(&spec.blocks, |id| node_ids.contains(id));
let mut g: Graph<NodeLabel, EdgeLabel> = Graph::with_options(GraphOptions {
directed: true,
multigraph: true,
compound: !tree.is_empty(),
});
let mut measured: HashMap<&str, &SpecNode> = HashMap::new();
for node in &spec.nodes {
g.set_node(
node.id.clone(),
Some(NodeLabel {
width: node.size.w,
height: node.size.h,
..NodeLabel::default()
}),
);
measured.insert(node.id.as_str(), node);
}
for c in tree.iter() {
g.set_node(c.id.clone(), Some(NodeLabel::default()));
}
for c in tree.iter() {
if let Some(parent) = &c.parent {
g.set_parent(&c.id, Some(parent.as_str()));
}
for m in &c.member_nodes {
if g.has_node(m) {
g.set_parent(m, Some(c.id.as_str()));
}
}
}
let mut drawable: Vec<Drawable> = Vec::new();
for edge in &spec.edges {
let is_node = |id: &str| measured.contains_key(id);
let (Some(tail), Some(head)) = (
tree.anchor(&edge.from, &is_node),
tree.anchor(&edge.to, &is_node),
) else {
continue;
};
let (tail, head) = (tail.to_string(), head.to_string());
let (w, h) = edge
.label
.as_ref()
.map_or((0.0, 0.0), |l| (l.width, l.height));
g.set_edge(
tail.clone(),
head.clone(),
Some(EdgeLabel {
width: w,
height: h,
minlen: edge.minlen.max(1) as i32,
..EdgeLabel::default()
}),
Some(edge.id.as_str()),
);
drawable.push(Drawable { edge, tail, head });
}
layout(
&mut g,
Some(LayoutOptions {
rankdir: rank_dir(spec.direction),
nodesep: NODE_SEP,
edgesep: EDGE_SEP,
ranksep: RANK_SEP,
marginx: MARGIN,
marginy: MARGIN,
tie_keep_first: true,
..LayoutOptions::default()
}),
);
let mut nodes: Vec<PlacedNode> = Vec::with_capacity(spec.nodes.len());
for node in &spec.nodes {
let placed = g.node(&node.id);
let center = Point::new(
placed.and_then(|n| n.x).unwrap_or(0.0),
placed.and_then(|n| n.y).unwrap_or(0.0),
);
nodes.push(PlacedNode {
id: node.id.clone(),
shape: node.glyph,
center,
size: node.size,
label: node.label.clone(),
panel: node.panel.clone(),
series: None,
mark: None,
});
}
if nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
let by_id: HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect();
let placed_clusters = read_clusters(&g, &tree, &nodes);
let mut placed_edges: Vec<PlacedEdge> = Vec::with_capacity(drawable.len());
for Drawable {
edge,
tail: tail_id,
head: head_id,
} in drawable
{
let raw = g
.edge(&tail_id, &head_id, Some(edge.id.as_str()))
.map(|l| l.points.clone())
.unwrap_or_default();
let (Some(&ti), Some(&hi)) = (by_id.get(tail_id.as_str()), by_id.get(head_id.as_str()))
else {
continue;
};
let end_of = |written: &str, anchor: &PlacedNode| match placed_clusters
.iter()
.find(|c| c.id == written)
{
Some(c) => edges::End::Cluster(clusters::Rect::new(&c.center, c.size)),
None => edges::End::Node(anchor.shape, anchor.center.clone(), anchor.size),
};
let tail = end_of(&edge.from, &nodes[ti]);
let head = end_of(&edge.to, &nodes[hi]);
let points = edges::route(&raw, &tail, &head);
let placed_label = edge.label.clone().and_then(|l| {
edges::arc_midpoint(&points).map(|center| PlacedEdgeLabel {
center,
size: Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0),
label: l,
})
});
let side_label = |label: &Option<Label>, at_start: bool| -> Option<PlacedEdgeLabel> {
let l = label.clone()?;
let center = edges::end_label_anchor(&points, at_start, l.width, l.height)?;
Some(PlacedEdgeLabel {
center,
size: Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0),
label: l,
})
};
let start_label = side_label(&edge.start_label, true);
let end_label = side_label(&edge.end_label, false);
placed_edges.push(PlacedEdge {
from: edge.from.clone(),
to: edge.to.clone(),
points,
tip_start: edge.tip_start,
tip_end: edge.tip_end,
stroke: edge.stroke,
label: placed_label,
start_label,
end_label,
badge: None,
series: None,
straight: false,
overlay: false,
});
}
let mut diagram = Diagram {
width: 0.0,
height: 0.0,
nodes,
edges: placed_edges,
clusters: placed_clusters,
lifelines: Vec::new(),
};
normalise(&mut diagram);
Ok(diagram)
}
struct Drawable<'a> {
edge: &'a SpecEdge,
tail: String,
head: String,
}
fn read_clusters(
g: &Graph<NodeLabel, EdgeLabel>,
tree: &clusters::Tree,
nodes: &[PlacedNode],
) -> Vec<PlacedCluster> {
let mut out: Vec<PlacedCluster> = Vec::new();
for c in tree.iter() {
let Some(n) = g.node(&c.id) else { continue };
let (Some(x), Some(y)) = (n.x, n.y) else {
continue;
};
out.push(PlacedCluster {
id: c.id.clone(),
title: Label::measure(&c.title),
center: Point::new(x, y),
size: Size::new(n.width, n.height),
parent: c.parent.clone(),
depth: c.depth,
dashed: c.dashed,
filled: true,
sections: Vec::new(),
});
}
let placed: HashSet<String> = out.iter().map(|c| c.id.clone()).collect();
for c in &mut out {
if c.parent.as_ref().is_some_and(|p| !placed.contains(p)) {
c.parent = None;
}
}
out.sort_by_key(|c| c.depth);
fit_titles(&mut out, tree, nodes);
out
}
fn fit_titles(clusters: &mut [PlacedCluster], tree: &clusters::Tree, nodes: &[PlacedNode]) {
let mut order: Vec<usize> = (0..clusters.len()).collect();
order.sort_by(|a, b| clusters[*b].depth.cmp(&clusters[*a].depth));
for i in order {
let id = clusters[i].id.clone();
let mut rect = clusters::Rect::new(&clusters[i].center, clusters[i].size);
let mut top_child = f64::INFINITY;
for c in clusters.iter() {
if c.parent.as_deref() == Some(id.as_str()) {
let child = clusters::Rect::new(&c.center, c.size);
rect.absorb(&child);
top_child = top_child.min(child.top);
}
}
if let Some(block) = tree.get(&id) {
for m in &block.member_nodes {
if let Some(n) = nodes.iter().find(|n| &n.id == m) {
let (l, t, r, b) = n.bounds();
rect.absorb(&clusters::Rect {
left: l,
top: t,
right: r,
bottom: b,
});
top_child = top_child.min(t);
}
}
}
let title = clusters[i].title.clone();
if !title.is_blank() {
let need = title.width + clusters::TITLE_PAD_X;
let short = need - rect.size().w;
if short > 0.0 {
rect.left -= short / 2.0;
rect.right += short / 2.0;
}
let need = title.height + clusters::TITLE_PAD_Y * 2.0;
if top_child.is_finite() {
let short = need - (top_child - rect.top);
if short > 0.0 {
rect.top -= short;
}
}
}
clusters[i].center = rect.center();
clusters[i].size = rect.size();
}
}
pub const LABEL_PAD_X: f64 = 4.0;
pub const LABEL_PAD_Y: f64 = 1.0;
fn normalise(diagram: &mut Diagram) {
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut grow = |x0: f64, y0: f64, x1: f64, y1: f64| {
min_x = min_x.min(x0);
min_y = min_y.min(y0);
max_x = max_x.max(x1);
max_y = max_y.max(y1);
};
for n in &diagram.nodes {
let (l, t, r, b) = n.bounds();
grow(l, t, r, b);
}
for c in &diagram.clusters {
let (l, t, r, b) = c.bounds();
grow(l, t, r, b);
}
for l in &diagram.lifelines {
let (left, right) = l.extent();
grow(left, l.top, right, l.bottom);
}
for e in &diagram.edges {
for p in e.drawn_points() {
grow(p.x, p.y, p.x, p.y);
}
for l in [&e.label, &e.start_label, &e.end_label, &e.badge]
.into_iter()
.flatten()
{
grow(
l.center.x - l.size.w / 2.0,
l.center.y - l.size.h / 2.0,
l.center.x + l.size.w / 2.0,
l.center.y + l.size.h / 2.0,
);
}
}
if !min_x.is_finite() {
return;
}
let (dx, dy) = (MARGIN - min_x, MARGIN - min_y);
for n in &mut diagram.nodes {
n.center = Point::new(n.center.x + dx, n.center.y + dy);
}
for c in &mut diagram.clusters {
c.center = Point::new(c.center.x + dx, c.center.y + dy);
for section in &mut c.sections {
section.y += dy;
}
}
for l in &mut diagram.lifelines {
l.x += dx;
l.top += dy;
l.bottom += dy;
for a in &mut l.activations {
a.top += dy;
a.bottom += dy;
}
}
for e in &mut diagram.edges {
for p in &mut e.points {
*p = Point::new(p.x + dx, p.y + dy);
}
for l in [
&mut e.label,
&mut e.start_label,
&mut e.end_label,
&mut e.badge,
]
.into_iter()
.flatten()
{
l.center = Point::new(l.center.x + dx, l.center.y + dy);
}
}
diagram.width = (max_x - min_x) + MARGIN * 2.0;
diagram.height = (max_y - min_y) + MARGIN * 2.0;
}
fn rank_dir(direction: Direction) -> RankDir {
match direction {
Direction::TopToBottom => RankDir::TB,
Direction::BottomToTop => RankDir::BT,
Direction::LeftToRight => RankDir::LR,
Direction::RightToLeft => RankDir::RL,
}
}