use crate::preview::mermaid::class::{self, ClassDiagram, Line, Marker};
use crate::preview::mermaid::flowchart::Stroke;
use crate::preview::mermaid::text_metrics;
use super::edges::Tip;
use super::panel::{self, Compartment};
use super::shapes::{self, Glyph};
use super::svg;
use super::{
lay_out_spec, Diagram, GraphSpec, Label, RenderError, SpecBlock, SpecEdge, SpecNode, Theme,
};
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let diagram = class::parse(code)?;
let laid = lay_out(&diagram)?;
Ok(svg::emit(&laid, &Theme::named(theme)))
}
pub fn lay_out(diagram: &ClassDiagram) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if diagram.classes.is_empty() && diagram.notes.is_empty() {
return Err(RenderError::NothingToDraw);
}
lay_out_spec(&spec_of(diagram))
}
pub fn spec_of(diagram: &ClassDiagram) -> GraphSpec {
let mut nodes: Vec<SpecNode> = Vec::new();
for c in &diagram.classes {
let panel = panel::class_panel(&compartments_of(c));
nodes.push(SpecNode {
id: c.id.clone(),
glyph: Glyph::ClassBox,
label: Label::measure(""),
size: shapes::size(Glyph::ClassBox, panel.size),
panel: Some(panel),
});
}
for n in &diagram.notes {
let label = Label::measure(&n.text);
let size = shapes::size(Glyph::Note, super::Size::new(label.width, label.height));
nodes.push(SpecNode {
id: n.id.clone(),
glyph: Glyph::Note,
label,
size,
panel: None,
});
}
let blocks = diagram
.namespaces
.iter()
.map(|n| SpecBlock {
id: n.id.clone(),
title: n.label.clone(),
members: n.members.clone(),
dashed: false,
})
.collect();
let mut edges: Vec<SpecEdge> = diagram
.relations
.iter()
.map(|r| SpecEdge {
id: r.id.clone(),
from: r.from.clone(),
to: r.to.clone(),
label: r
.label
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
tip_start: tip_of(r.start),
tip_end: tip_of(r.end),
stroke: match r.line {
Line::Solid => Stroke::Normal,
Line::Dotted => Stroke::Dotted,
},
minlen: 1,
start_label: r
.start_cardinality
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
end_label: r
.end_cardinality
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
})
.collect();
for n in &diagram.notes {
let Some(target) = &n.target else { continue };
edges.push(SpecEdge {
id: format!("edge{}", n.id),
from: n.id.clone(),
to: target.clone(),
label: None,
tip_start: Tip::None,
tip_end: Tip::None,
stroke: Stroke::Dotted,
minlen: 1,
start_label: None,
end_label: None,
});
}
GraphSpec {
direction: diagram.direction,
nodes,
edges,
blocks,
}
}
fn compartments_of(c: &class::Class) -> Vec<Compartment> {
let mut top: Vec<Label> = c
.annotations
.iter()
.map(|a| Label::measure(&format!("«{a}»")))
.collect();
top.push(Label::measure(&c.label));
let mut out = vec![Compartment {
lines: top,
centered: true,
}];
let attributes: Vec<Label> = c
.attributes
.iter()
.map(|m| Label::measure(&m.display()))
.collect();
let methods: Vec<Label> = c
.methods
.iter()
.map(|m| Label::measure(&m.display()))
.collect();
if attributes.is_empty() && methods.is_empty() {
out.push(Compartment {
lines: Vec::new(),
centered: false,
});
return out;
}
if !attributes.is_empty() {
out.push(Compartment {
lines: attributes,
centered: false,
});
}
if !methods.is_empty() {
out.push(Compartment {
lines: methods,
centered: false,
});
}
out
}
fn tip_of(marker: Marker) -> Tip {
match marker {
Marker::None => Tip::None,
Marker::Extension => Tip::HollowTriangle,
Marker::Composition => Tip::FilledDiamond,
Marker::Aggregation => Tip::HollowDiamond,
Marker::Dependency => Tip::Arrow,
Marker::Lollipop => Tip::Lollipop,
}
}