use crate::completeness::Completeness;
use crate::{Label, Scope, ScopeGraph};
use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
mod traverse;
#[derive(Default, Copy, Clone, Debug)]
pub enum Target {
Dot,
#[default]
Mermaid,
}
pub struct RenderSettings {
pub show_edge_labels: bool,
pub title: Option<String>,
pub target: Target,
}
impl RenderSettings {
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.title = Some(name.as_ref().to_string());
self
}
}
impl Default for RenderSettings {
fn default() -> Self {
Self {
show_edge_labels: true,
title: None,
target: Default::default(),
}
}
}
pub struct EdgeStyle {}
pub struct Edge {
pub from: Scope,
pub to: EdgeTo,
}
pub struct EdgeTo {
pub to: Scope,
pub edge_style: EdgeStyle,
pub label_text: String,
}
pub trait RenderScopeLabel: Label {
fn render(&self) -> String;
}
pub trait RenderScopeData {
fn render_node(&self) -> Option<String> {
None
}
fn render_node_label(&self) -> Option<String> {
None
}
fn extra_edges(&self) -> Vec<EdgeTo> {
Vec::new()
}
fn definition(&self) -> bool {
self.render_node().is_some()
}
}
fn scope_to_node_name(s: Scope) -> String {
format!("scope_{}", s.0)
}
fn escape_text_dot(inp: &str) -> String {
inp.replace('"', "\\\"")
}
fn escape_text_mermaid(inp: &str) -> String {
inp.replace('"', "&quot;")
}
impl<
LABEL: Clone + RenderScopeLabel + Label,
DATA: RenderScopeData + Clone,
CMPL: Completeness<LABEL, DATA>,
> ScopeGraph<'_, LABEL, DATA, CMPL>
{
pub fn render<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
match settings.target {
Target::Dot => self.render_dot(output, settings),
Target::Mermaid => self.render_mermaid(output, settings),
}
}
fn render_mermaid<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
let (mut edges, nodes) = traverse::traverse(self);
if let Some(ref i) = settings.title {
writeln!(output, "---")?;
writeln!(output, r#"title: {}"#, escape_text_mermaid(i))?;
writeln!(output, "---")?;
}
writeln!(output, "flowchart LR")?;
for (scope, data) in nodes {
edges.extend(
data.extra_edges()
.into_iter()
.map(|to| Edge { from: scope, to }),
);
let name = scope_to_node_name(scope);
let mut attrs = Vec::new();
let label =
escape_text_mermaid(&data.render_node().unwrap_or_else(|| scope.0.to_string()));
if let Some(label) = data.render_node_label() {
attrs.push(format!(r#"[xlabel="{}"]"#, escape_text_mermaid(&label)))
}
attrs.push(r#"[penwidth="2.0"]"#.to_string());
if data.definition() {
writeln!(output, r#" {name}["{label}"]"#)?;
} else {
writeln!(output, r#" {name}(("{label}"))"#)?;
}
}
for edge in edges {
let from = scope_to_node_name(edge.from);
let to = scope_to_node_name(edge.to.to);
let label = escape_text_mermaid(&edge.to.label_text);
if settings.show_edge_labels {
writeln!(output, r#"{from} ==>|"{label}"| {to}"#)?
} else {
writeln!(output, " {from} ==> {to}")?
}
}
Ok(())
}
fn render_dot<W: Write>(&self, output: &mut W, settings: RenderSettings) -> io::Result<()> {
let (mut edges, nodes) = traverse::traverse(self);
writeln!(output, "digraph {{")?;
writeln!(
output,
r#"node [colorscheme="ylgnbu6",width="0.1",height="0.1",margin="0.01",xlp="b"]"#
)?;
if let Some(ref i) = settings.title {
writeln!(output, r#"labelloc="t";"#)?;
writeln!(output, r#"label="{}";"#, escape_text_dot(i))?;
}
writeln!(output, r#"splines=false;"#)?;
for (scope, data) in nodes {
edges.extend(
data.extra_edges()
.into_iter()
.map(|to| Edge { from: scope, to }),
);
let name = scope_to_node_name(scope);
let mut attrs = Vec::new();
if data.definition() {
attrs.push(r#"[shape="square"]"#.to_string())
} else {
attrs.push(r#"[shape="circle"]"#.to_string())
};
let label = escape_text_dot(&data.render_node().unwrap_or_else(|| scope.0.to_string()));
attrs.push(format!(r#"[label="{label}"]"#));
if let Some(label) = data.render_node_label() {
attrs.push(format!(r#"[xlabel="{}"]"#, escape_text_dot(&label)))
}
attrs.push(r#"[penwidth="2.0"]"#.to_string());
writeln!(output, r#"{name} {}"#, attrs.join(""))?
}
for edge in edges {
let from = scope_to_node_name(edge.from);
let to = scope_to_node_name(edge.to.to);
let label = edge.to.label_text;
if settings.show_edge_labels {
writeln!(output, "{from} -> {to} [label={label}]")?
} else {
writeln!(output, "{from} -> {to}")?
}
}
writeln!(output, "}}")?;
Ok(())
}
pub fn render_to(
&self,
path: impl AsRef<Path>,
mut settings: RenderSettings,
) -> io::Result<()> {
let path = path.as_ref();
let mut w = File::create(path)?;
if settings.title.is_none() {
settings.title = Some(
path.file_stem()
.expect("path must have filename for File::create to work")
.to_string_lossy()
.to_string(),
);
}
self.render(&mut w, settings)
}
}