use crate::preview::mermaid::c4::{Boundary, Element, ShapeKind, C4};
use crate::preview::mermaid::flowchart::{Shape, Stroke};
use crate::preview::mermaid::text_metrics;
use super::panel::{class_panel, Compartment};
use super::{
lay_out_spec, shapes, svg, Curve, Diagram, Glyph, GraphSpec, Label, RenderError, Size,
SpecBlock, SpecEdge, SpecNode, Theme, Tip,
};
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let diagram = crate::preview::mermaid::c4::parse(code)?;
let laid = lay_out(&diagram)?;
Ok(svg::emit(&laid, &Theme::named(theme)))
}
pub fn lay_out(diagram: &C4) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if diagram.elements.is_empty() {
return Err(RenderError::NothingToDraw);
}
lay_out_spec(&spec_of(diagram))
}
pub fn spec_of(diagram: &C4) -> GraphSpec {
let nodes: Vec<SpecNode> = diagram.elements.iter().map(element_node).collect();
let blocks: Vec<SpecBlock> = diagram
.boundaries
.iter()
.map(|b| SpecBlock {
id: b.alias.clone(),
title: frame_title(b),
members: b.members.clone(),
dashed: true,
})
.collect();
let edges: Vec<SpecEdge> = diagram
.rels
.iter()
.enumerate()
.map(|(i, r)| {
let text = if r.techn.trim().is_empty() {
r.label.clone()
} else {
format!("{}\n[{}]", r.label, r.techn)
};
SpecEdge {
id: format!("rel{i}"),
from: r.from.clone(),
to: r.to.clone(),
label: Some(Label::measure(&text)).filter(|l| !l.is_blank()),
tip_start: if r.bidirectional {
Tip::Arrow
} else {
Tip::None
},
tip_end: Tip::Arrow,
stroke: Stroke::Normal,
minlen: 1,
start_label: None,
end_label: None,
style: None,
curve: Curve::Basis,
}
})
.collect();
GraphSpec {
direction: diagram.direction,
nodes,
edges,
blocks,
}
}
fn element_node(e: &Element) -> SpecNode {
let glyph = match e.shape {
ShapeKind::Person => Glyph::Actor,
ShapeKind::Box => Glyph::Flow(Shape::Rect),
ShapeKind::Database => Glyph::Flow(Shape::Cylinder),
ShapeKind::Queue => Glyph::Flow(Shape::Stadium),
};
let lines = caption(e);
if glyph == Glyph::Actor {
let mut panel = class_panel(&[Compartment {
lines: lines.iter().map(|l| Label::measure(l)).collect(),
centered: true,
}]);
for row in &mut panel.rows {
row.y += shapes::ACTOR_FIGURE_HEIGHT;
}
panel
.rules
.iter_mut()
.for_each(|r| *r += shapes::ACTOR_FIGURE_HEIGHT);
panel.size = Size::new(panel.size.w, panel.size.h + shapes::ACTOR_FIGURE_HEIGHT);
return SpecNode {
id: e.alias.clone(),
glyph,
label: Label::measure(""),
size: shapes::size(glyph, panel.size),
panel: Some(panel),
style: None,
};
}
let label = Label::measure(&lines.join("\n"));
SpecNode {
id: e.alias.clone(),
glyph,
size: shapes::size(glyph, Size::new(label.width, label.height)),
label,
panel: None,
style: None,
}
}
fn caption(e: &Element) -> Vec<String> {
let mut out = vec![e.label.clone()];
if !e.kind_line.trim().is_empty() {
let external = if e.external { ", external" } else { "" };
out.push(format!("[{}{external}]", e.kind_line));
}
if !e.descr.trim().is_empty() {
out.push(e.descr.clone());
}
out
}
fn frame_title(b: &Boundary) -> String {
if b.kind_line.trim().is_empty() {
return b.label.clone();
}
format!("{}\n[{}]", b.label, b.kind_line)
}