mod dot;
mod mermaid;
use std::borrow::Cow;
pub use dot::{DotFormat, DotFormatter};
pub use mermaid::{MermaidFormat, MermaidFormatter};
use self::mermaid::encode_label;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PresentationStyle {
pub color: Option<String>,
pub fill: Option<String>,
pub stroke: Option<String>,
pub stroke_width: Option<String>,
}
impl PresentationStyle {
pub const fn new() -> Self {
Self {
color: None,
fill: None,
stroke: None,
stroke_width: None,
}
}
pub const fn is_empty(&self) -> bool {
self.color.is_none()
&& self.fill.is_none()
&& self.stroke.is_none()
&& self.stroke_width.is_none()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NodeStyle {
Hidden,
#[non_exhaustive]
Boxed {
label: String,
attrs: PresentationStyle,
},
}
impl NodeStyle {
pub fn boxed(label: impl ToString) -> Self {
Self::Boxed {
label: label.to_string(),
attrs: Default::default(),
}
}
pub fn with_attrs(mut self, attrs: PresentationStyle) -> Self {
if let Self::Boxed { attrs: a, .. } = &mut self {
*a = attrs
}
self
}
pub fn attrs(&self) -> &PresentationStyle {
static DEFAULT: PresentationStyle = PresentationStyle::new();
match self {
Self::Boxed { attrs, .. } => attrs,
_ => &DEFAULT,
}
}
}
impl Default for NodeStyle {
fn default() -> Self {
Self::Boxed {
label: String::new(),
attrs: Default::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PortStyle {
Hidden,
Plain(String, bool),
Boxed(String, bool),
}
impl PortStyle {
pub fn new(label: impl ToString) -> Self {
Self::Boxed(label.to_string(), true)
}
pub fn text(label: impl ToString, show_offset: bool) -> Self {
Self::Plain(label.to_string(), show_offset)
}
pub fn boxed(label: impl ToString, show_offset: bool) -> Self {
Self::Boxed(label.to_string(), show_offset)
}
}
impl Default for PortStyle {
fn default() -> Self {
Self::Boxed(String::new(), true)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum EdgeStyle {
#[default]
Solid,
Dotted,
Dashed,
Labelled(String, Box<EdgeStyle>),
Custom(String),
}
impl EdgeStyle {
pub fn with_label(self, label: impl ToString) -> Self {
match self {
Self::Labelled(_, e) => Self::Labelled(label.to_string(), e),
_ => Self::Labelled(label.to_string(), Box::new(self)),
}
}
pub fn strip_label(&self) -> &Self {
match self {
Self::Labelled(_, e) => e.strip_label(),
e => e,
}
}
pub(super) fn as_dot_str(&self) -> &str {
match self {
Self::Solid => "",
Self::Dotted => "dotted",
Self::Dashed => "dashed",
Self::Custom(s) => s,
Self::Labelled(_lbl, e) => e.as_dot_str(),
}
}
pub(super) fn as_mermaid_str(&self) -> Cow<'_, str> {
match self {
Self::Solid => "-->".into(),
Self::Dotted => "-.->".into(),
Self::Dashed => "-.->".into(),
Self::Custom(s) => s.into(),
Self::Labelled(lbl, e) => {
let lbl = encode_label("", lbl);
match e.strip_label() {
Self::Solid => format!("--{lbl}-->").into(),
Self::Dotted => format!("-.{lbl}.->").into(),
Self::Dashed => format!("-.{lbl}.->").into(),
Self::Custom(s) => s.into(),
Self::Labelled(_, _) => {
unreachable!("`strip_label` cannot return a `Labelled`")
}
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::type_complexity)]
mod test {
use std::fmt::Display;
use std::sync::OnceLock;
use rstest::{fixture, rstest};
use crate::view::Region;
use crate::LinkMut;
use crate::PortMut;
use crate::PortView;
use super::{DotFormat, MermaidFormat, NodeStyle, PresentationStyle};
type PortGraph = crate::PortGraph<u32, u32, u16>;
type Hierarchy = crate::Hierarchy<u32>;
type NodeIndex = crate::NodeIndex<u32>;
type Weights<N, P> = crate::Weights<N, P, u32, u32>;
#[fixture]
fn flat_graph() -> (
&'static str,
PortGraph,
Option<Hierarchy>,
Option<Weights<String, String>>,
Option<fn(NodeIndex) -> NodeStyle>,
) {
let mut graph = PortGraph::new();
let n1 = graph.add_node(3, 2);
let n2 = graph.add_node(1, 0);
let n3 = graph.add_node(1, 0);
graph.link_nodes(n1, 0, n2, 0).unwrap();
graph.link_nodes(n1, 1, n3, 0).unwrap();
("flat", graph, None, None, None)
}
#[fixture]
fn hierarchy_graph() -> (
&'static str,
PortGraph,
Option<Hierarchy>,
Option<Weights<String, String>>,
Option<impl FnMut(NodeIndex) -> NodeStyle>,
) {
let mut graph = PortGraph::new();
let n1 = graph.add_node(3, 2);
let n2 = graph.add_node(0, 1);
let n3 = graph.add_node(1, 0);
graph.link_nodes(n2, 0, n3, 0).unwrap();
let mut hier = Hierarchy::new();
hier.push_child(n2, n1).unwrap();
hier.push_child(n3, n1).unwrap();
let node_style = move |n: NodeIndex| {
if n == n1 {
NodeStyle::boxed("root").with_attrs(PresentationStyle {
color: Some("#f00".to_string()),
fill: Some("#0f0".to_string()),
stroke: Some("#00f".to_string()),
stroke_width: Some("4px".to_string()),
})
} else {
NodeStyle::boxed(n.index())
}
};
("hierarchy", graph, Some(hier), None, Some(node_style))
}
#[fixture]
fn hierarchy_interregional_graph() -> (
&'static str,
PortGraph,
Option<Hierarchy>,
Option<Weights<String, String>>,
Option<fn(NodeIndex) -> NodeStyle>,
) {
let mut graph = PortGraph::new();
let n1 = graph.add_node(3, 2);
let n2 = graph.add_node(0, 1);
let n3 = graph.add_node(1, 0);
let n4 = graph.add_node(1, 1);
let n5 = graph.add_node(1, 1);
graph.link_nodes(n2, 0, n3, 0).unwrap();
graph.link_nodes(n4, 0, n5, 0).unwrap();
let mut hier = Hierarchy::new();
hier.push_child(n2, n1).unwrap();
hier.push_child(n3, n1).unwrap();
hier.push_child(n4, n2).unwrap();
hier.push_child(n5, n3).unwrap();
("hierarchy_interregional", graph, Some(hier), None, None)
}
#[fixture]
fn weighted_graph() -> (
&'static str,
PortGraph,
Option<Hierarchy>,
Option<Weights<String, String>>,
Option<fn(NodeIndex) -> NodeStyle>,
) {
let mut graph = PortGraph::new();
let n1 = graph.add_node(0, 2);
let n2 = graph.add_node(1, 0);
let n3 = graph.add_node(1, 0);
let p10 = graph.output(n1, 0).unwrap();
let p11 = graph.output(n1, 1).unwrap();
let p20 = graph.input(n2, 0).unwrap();
let p30 = graph.input(n3, 0).unwrap();
graph.link_ports(p10, p20).unwrap();
graph.link_ports(p11, p30).unwrap();
let mut weights = Weights::new();
weights[n1] = "node1".to_string();
weights[n2] = "node2".to_string();
weights[n3] = "node3".to_string();
weights[p10] = "out 0".to_string();
weights[p11] = "out 1".to_string();
weights[p20] = "in 0".to_string();
weights[p30] = "in 0".to_string();
("weighted", graph, None, Some(weights), None)
}
#[allow(clippy::type_complexity)]
#[fixture]
fn region_view() -> (
&'static str,
Region<'static, PortGraph>,
Option<Hierarchy>,
Option<Weights<String, String>>,
Option<fn(NodeIndex) -> NodeStyle>,
) {
let mut graph = PortGraph::new();
let other = graph.add_node(42, 0);
let root = graph.add_node(1, 0);
let a = graph.add_node(1, 2);
let b = graph.add_node(0, 0);
let c = graph.add_node(0, 0);
graph.link_nodes(a, 0, other, 0).unwrap();
graph.link_nodes(a, 1, root, 0).unwrap();
static HIERARCHY: OnceLock<Hierarchy> = OnceLock::new();
let hierarchy = HIERARCHY.get_or_init(|| {
let mut hierarchy = Hierarchy::new();
hierarchy.push_child(root, other).unwrap();
hierarchy.push_child(a, root).unwrap();
hierarchy.push_child(b, root).unwrap();
hierarchy.push_child(c, b).unwrap();
hierarchy
});
let region = Region::new(graph, hierarchy, root);
("region_view", region, Some(hierarchy.clone()), None, None)
}
#[rstest]
#[case::flat(flat_graph())]
#[case::hierarchy(hierarchy_graph())]
#[case::interregional(hierarchy_interregional_graph())]
#[case::weighted(weighted_graph())]
#[case::region_view(region_view())]
#[cfg_attr(miri, ignore)] fn mermaid_output<WN: Display + Clone, WP>(
#[case] graph_elems: (
&str,
impl MermaidFormat<NodeIndexBase = u32, PortIndexBase = u32>,
Option<Hierarchy>,
Option<Weights<WN, WP>>,
Option<impl FnMut(NodeIndex) -> NodeStyle>,
),
) {
let (name, graph, hierarchy, weights, node_style) = graph_elems;
let fmt = graph.mermaid_format();
let fmt = match &hierarchy {
Some(h) => fmt.with_hierarchy(h),
None => fmt,
};
let fmt = match node_style {
Some(node_style) => fmt.with_node_style(node_style),
None => fmt,
};
let fmt = match &weights {
Some(w) => fmt.with_weights(w),
None => fmt,
};
let mermaid = fmt.finish();
let name = format!("{name}__mermaid");
insta::assert_snapshot!(name, mermaid);
}
#[rstest]
#[case::flat(flat_graph())]
#[case::hierarchy(hierarchy_graph())]
#[case::interregional(hierarchy_interregional_graph())]
#[case::weighted(weighted_graph())]
#[cfg_attr(miri, ignore)] fn dot_output<WN: Display + Clone, WP: Display + Clone>(
#[case] graph_elems: (
&str,
impl DotFormat<NodeIndexBase = u32, PortIndexBase = u32>,
Option<Hierarchy>,
Option<Weights<WN, WP>>,
Option<impl FnMut(NodeIndex) -> NodeStyle>,
),
) {
let (name, graph, hierarchy, weights, node_style) = graph_elems;
let fmt = graph.dot_format();
let fmt = match &hierarchy {
Some(h) => fmt.with_hierarchy(h),
None => fmt,
};
let fmt = match node_style {
Some(node_style) => fmt.with_node_style(node_style),
None => fmt,
};
let fmt = match &weights {
Some(w) => fmt.with_weights(w),
None => fmt,
};
let dot = fmt.finish();
let name = format!("{name}__dot");
insta::assert_snapshot!(name, dot);
}
}