mod layout;
pub use layout::MermaidLayout;
use std::collections::HashMap;
use std::fmt::Display;
use itertools::Itertools;
use crate::algorithms::{lca, LCA};
use crate::{Hierarchy, LinkView, NodeIndex, Weights};
use super::{EdgeStyle, NodeStyle, PresentationStyle};
const INDENTATION_SEPARATOR: &str = " ";
#[allow(clippy::type_complexity)]
pub struct MermaidFormatter<'g, G: LinkView> {
graph: &'g G,
forest: Option<&'g Hierarchy<G::NodeIndexBase>>,
node_style: Option<Box<dyn FnMut(NodeIndex<G::NodeIndexBase>) -> NodeStyle + 'g>>,
#[allow(clippy::type_complexity)]
edge_style: Option<Box<dyn FnMut(G::LinkEndpoint, G::LinkEndpoint) -> EdgeStyle + 'g>>,
layout: MermaidLayout,
}
impl<'g, G> MermaidFormatter<'g, G>
where
G: LinkView,
{
pub fn new(graph: &'g G) -> Self {
Self {
graph,
forest: None,
node_style: None,
edge_style: None,
layout: MermaidLayout::default(),
}
}
pub fn with_hierarchy(mut self, forest: &'g Hierarchy<G::NodeIndexBase>) -> Self {
self.forest = Some(forest);
self
}
pub fn with_node_style(
mut self,
node_style: impl FnMut(NodeIndex<G::NodeIndexBase>) -> NodeStyle + 'g,
) -> Self {
self.node_style = Some(Box::new(node_style));
self
}
pub fn with_edge_style(
mut self,
edge_style: impl FnMut(G::LinkEndpoint, G::LinkEndpoint) -> EdgeStyle + 'g,
) -> Self {
self.edge_style = Some(Box::new(edge_style));
self
}
pub fn with_weights<'w, N, P>(
self,
weights: &'w Weights<N, P, G::NodeIndexBase, G::PortIndexBase>,
) -> Self
where
'w: 'g,
N: Display + Clone,
{
self.with_node_style(|n| NodeStyle::boxed(&weights.nodes[n]))
}
pub fn with_layout(mut self, layout: MermaidLayout) -> Self {
self.layout = layout;
self
}
pub fn finish(mut self) -> String {
let mut mermaid =
MermaidBuilder::init(self.node_style.take(), self.edge_style.take(), self.layout);
self.explore_forest(&mut mermaid);
mermaid.finish()
}
fn explore_forest(&self, mmd: &mut MermaidBuilder<'g, G>) {
let mut exploration_stack: Vec<ExplorationStep<G::NodeIndexBase>> = Vec::new();
for root in self.graph.nodes_iter().filter(|n| self.is_root(*n)) {
exploration_stack.push(ExplorationStep::ExploreNode { node: root });
}
#[allow(clippy::type_complexity)]
let mut edges: HashMap<
Option<NodeIndex<G::NodeIndexBase>>,
Vec<(
NodeIndex<G::NodeIndexBase>,
G::LinkEndpoint,
NodeIndex<G::NodeIndexBase>,
G::LinkEndpoint,
)>,
> = HashMap::new();
let lca: Option<LCA<G::NodeIndexBase>> = self.forest.map(|f| lca(self.graph, f));
while let Some(instr) = exploration_stack.pop() {
match instr {
ExplorationStep::ExploreNode { node } => {
if self.is_leaf(node) {
mmd.add_leaf(node);
} else {
mmd.start_subgraph(node);
exploration_stack.push(ExplorationStep::ExitSubgraph { subgraph: node });
for child in self.node_children(node).rev() {
exploration_stack.push(ExplorationStep::ExploreNode { node: child });
}
}
for (src, tgt) in self.graph.output_links(node) {
let src_node = self.graph.port_node(src).unwrap();
let tgt_node = self.graph.port_node(tgt).unwrap();
let lca = lca.as_ref().and_then(|l| l.lca(src_node, tgt_node));
edges
.entry(lca)
.or_insert_with(Vec::new)
.push((src_node, src, tgt_node, tgt));
}
}
ExplorationStep::ExitSubgraph { subgraph } => {
if let Some(es) = edges.remove(&Some(subgraph)) {
for (src_node, src, tgt_node, tgt) in es {
mmd.add_link(src_node, src, tgt_node, tgt);
}
}
mmd.end_subgraph();
}
}
}
for (_, es) in edges {
for (src_node, src, tgt_node, tgt) in es {
mmd.add_link(src_node, src, tgt_node, tgt);
}
}
}
fn is_root(&self, node: NodeIndex<G::NodeIndexBase>) -> bool {
let Some(f) = &self.forest else {
return true;
};
f.is_root(node)
|| f.parent(node)
.map_or(true, |p| !self.graph.contains_node(p))
}
fn is_leaf(&self, node: NodeIndex<G::NodeIndexBase>) -> bool {
self.forest.map_or(true, |f| !f.has_children(node))
}
fn node_children(
&self,
node: NodeIndex<G::NodeIndexBase>,
) -> impl DoubleEndedIterator<Item = NodeIndex<G::NodeIndexBase>> + '_ {
self.forest.iter().flat_map(move |f| f.children(node))
}
}
enum ExplorationStep<N: crate::index::IndexBase> {
ExploreNode { node: NodeIndex<N> },
ExitSubgraph { subgraph: NodeIndex<N> },
}
pub trait MermaidFormat: LinkView + Sized {
fn mermaid_format(&self) -> MermaidFormatter<'_, Self>;
fn mermaid_string(&self) -> String {
self.mermaid_format().finish()
}
}
impl<G> MermaidFormat for G
where
G: LinkView,
{
fn mermaid_format(&self) -> MermaidFormatter<'_, Self> {
MermaidFormatter::new(self)
}
}
#[allow(clippy::type_complexity)]
struct MermaidBuilder<'g, G: LinkView> {
output: String,
indent: usize,
node_style: Option<Box<dyn FnMut(NodeIndex<G::NodeIndexBase>) -> NodeStyle + 'g>>,
#[allow(clippy::type_complexity)]
edge_style: Option<Box<dyn FnMut(G::LinkEndpoint, G::LinkEndpoint) -> EdgeStyle + 'g>>,
}
impl<'g, G: LinkView> MermaidBuilder<'g, G> {
#[allow(clippy::type_complexity)]
pub fn init(
node_style: Option<Box<dyn FnMut(NodeIndex<G::NodeIndexBase>) -> NodeStyle + 'g>>,
edge_style: Option<Box<dyn FnMut(G::LinkEndpoint, G::LinkEndpoint) -> EdgeStyle + 'g>>,
layout: MermaidLayout,
) -> Self {
let mut builder = Self {
output: String::new(),
indent: 0,
node_style,
edge_style,
};
builder.emit_config(layout);
builder.push_line("graph LR");
builder.indent += 1;
builder
}
pub fn push_line(&mut self, s: impl AsRef<str>) {
let extra_capacity = self.indent * INDENTATION_SEPARATOR.len() + s.as_ref().len() + 1;
self.output.reserve(extra_capacity);
self.output
.push_str(&INDENTATION_SEPARATOR.repeat(self.indent));
self.output.push_str(s.as_ref());
self.output.push('\n');
}
fn push_strings(&mut self, strings: &[&str]) {
let extra_capacity = self.indent * INDENTATION_SEPARATOR.len()
+ strings.iter().map(|s| s.len()).sum::<usize>()
+ 1;
self.output.reserve(extra_capacity);
self.output
.push_str(&INDENTATION_SEPARATOR.repeat(self.indent));
for s in strings {
self.output.push_str(s);
}
self.output.push('\n');
}
pub fn add_leaf(&mut self, node: NodeIndex<G::NodeIndexBase>) {
let style = self
.node_style
.as_mut()
.map_or_else(NodeStyle::default, |f| f(node));
let id = node.index().to_string();
match style {
NodeStyle::Hidden => self.push_strings(&[id.as_ref(), ":::hidden"]),
NodeStyle::Boxed { label, attrs } => {
self.push_strings(&[id.as_ref(), "[", &encode_label(&id, &label), "]"]);
if !attrs.is_empty() {
self.push_strings(&[
"style ",
id.as_ref(),
" ",
&encode_presentation_attrs(&attrs),
]);
}
}
}
}
pub fn emit_config(&mut self, layout: MermaidLayout) {
if !layout.requires_config() {
return;
}
self.push_line("---");
self.push_line("config:");
self.indent += 1;
layout.emit_config(self);
self.indent -= 1;
self.push_line("---");
}
pub fn start_subgraph(&mut self, node: NodeIndex<G::NodeIndexBase>) {
let style = self
.node_style
.as_mut()
.map_or_else(NodeStyle::default, |f| f(node));
let id = node.index().to_string();
match &style {
NodeStyle::Hidden => self.push_strings(&["subgraph ", id.as_ref(), " [ ]"]),
NodeStyle::Boxed { label, .. } => self.push_strings(&[
"subgraph ",
id.as_ref(),
" [",
&encode_label(&id, label),
"]",
]),
}
self.indent += 1;
self.push_line("direction LR");
if !style.attrs().is_empty() {
self.push_strings(&[
"style ",
id.as_ref(),
" ",
&encode_presentation_attrs(style.attrs()),
]);
}
}
pub fn end_subgraph(&mut self) {
self.indent -= 1;
self.push_line("end");
}
pub fn add_link(
&mut self,
src_node: NodeIndex<G::NodeIndexBase>,
src: G::LinkEndpoint,
tgt_node: NodeIndex<G::NodeIndexBase>,
tgt: G::LinkEndpoint,
) {
let style = self
.edge_style
.as_mut()
.map_or_else(EdgeStyle::default, |f| f(src, tgt));
let src = src_node.index().to_string();
let tgt = tgt_node.index().to_string();
self.push_strings(&[&src, &style.as_mermaid_str(), &tgt]);
}
pub fn finish(self) -> String {
self.output
}
}
pub fn encode_label(id: &str, label: &str) -> String {
if label.is_empty() {
return id.to_string();
}
format!("\"{}\"", label.replace('"', "#quot;").replace('\n', "<br>"))
}
pub fn encode_presentation_attrs(attrs: &PresentationStyle) -> String {
let mut result = Vec::new();
if let Some(color) = &attrs.color {
result.push(format!("color:{color}"));
}
if let Some(fill) = &attrs.fill {
result.push(format!("fill:{fill}"));
}
if let Some(stroke) = &attrs.stroke {
result.push(format!("stroke:{stroke}"));
}
if let Some(stroke_width) = &attrs.stroke_width {
result.push(format!("stroke-width:{stroke_width}"));
}
result.into_iter().join(",")
}