use crate::preview::mermaid::flowchart::Stroke;
use crate::preview::mermaid::requirement::{Element, Relation, Requirement, RequirementDiagram};
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, SpecEdge,
SpecNode, Theme, Tip,
};
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let diagram = crate::preview::mermaid::requirement::parse(code)?;
let laid = lay_out(&diagram)?;
Ok(svg::emit(&laid, &Theme::named(theme)))
}
pub fn lay_out(diagram: &RequirementDiagram) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if diagram.requirements.is_empty() && diagram.elements.is_empty() {
return Err(RenderError::NothingToDraw);
}
lay_out_spec(&spec_of(diagram))
}
pub fn spec_of(diagram: &RequirementDiagram) -> GraphSpec {
let mut nodes: Vec<SpecNode> = Vec::new();
for r in &diagram.requirements {
nodes.push(box_node(&r.name, &requirement_rows(r)));
}
for e in &diagram.elements {
nodes.push(box_node(&e.name, &element_rows(e)));
}
let edges: Vec<SpecEdge> = diagram
.links
.iter()
.enumerate()
.map(|(i, link)| SpecEdge {
id: format!("r{i}"),
from: link.src.clone(),
to: link.dst.clone(),
label: Some(Label::measure(link.relation.word())),
tip_start: Tip::None,
tip_end: Tip::Arrow,
stroke: if link.relation == Relation::Contains {
Stroke::Normal
} else {
Stroke::Dotted
},
minlen: 1,
start_label: None,
end_label: None,
style: None,
curve: Curve::Basis,
})
.collect();
GraphSpec {
direction: diagram.direction,
nodes,
edges,
blocks: Vec::new(),
..GraphSpec::default()
}
}
fn box_node(name: &str, rows: &[(&'static str, String)]) -> SpecNode {
let mut compartments = vec![Compartment {
lines: vec![Label::measure(&format!("«{}»", rows[0].1))],
centered: true,
}];
compartments.push(Compartment {
lines: vec![Label::measure(name)],
centered: true,
});
let fields: Vec<Label> = rows[1..]
.iter()
.filter(|(_, v)| !v.trim().is_empty())
.map(|(k, v)| Label::measure(&format!("{k}: {v}")))
.collect();
compartments.push(Compartment {
lines: fields,
centered: false,
});
let panel = class_panel(&compartments);
SpecNode {
id: name.to_string(),
glyph: Glyph::ClassBox,
label: Label::measure(""),
size: shapes::size(Glyph::ClassBox, panel.size),
panel: Some(panel),
style: None,
}
}
fn requirement_rows(r: &Requirement) -> Vec<(&'static str, String)> {
vec![
("", r.kind.title().to_string()),
("Id", r.id.clone()),
("Text", r.text.clone()),
("Risk", r.risk.clone()),
("Verification", r.verify_method.clone()),
]
}
fn element_rows(e: &Element) -> Vec<(&'static str, String)> {
vec![
("", "Element".to_string()),
("Type", e.kind.clone()),
("Doc Ref", e.doc_ref.clone()),
]
}