pub mod backend;
mod charset;
mod edges;
mod shapes;
mod subgraph;
use crate::grid::Grid;
use crate::pathfinding::PathGrid;
use crate::types::{DiagramWarning, Graph, Node, RenderOptions};
use charset::{ASCII_CHARS, UNICODE_CHARS};
use edges::draw_edge;
use shapes::draw_node;
use subgraph::{draw_subgraph, protect_subgraph_borders};
fn build_path_grid(graph: &Graph, width: usize, height: usize) -> PathGrid {
let mut path_grid = PathGrid::new(width, height);
for node in graph.nodes.values() {
path_grid.block_rect(node.x, node.y, node.width, node.height);
}
for sg in &graph.subgraphs {
if sg.width > 0 && sg.height > 0 {
path_grid.block_rect(sg.x, sg.y, sg.width, 1);
path_grid.block_rect(sg.x, sg.y + sg.height.saturating_sub(1), sg.width, 1);
path_grid.block_rect(sg.x, sg.y, 1, sg.height);
path_grid.block_rect(sg.x + sg.width.saturating_sub(1), sg.y, 1, sg.height);
}
}
path_grid
}
pub fn render_graph(
graph: &Graph,
options: &RenderOptions,
warnings: &mut Vec<DiagramWarning>,
) -> String {
let chars = if options.ascii {
&ASCII_CHARS
} else {
&UNICODE_CHARS
};
let mut max_x = 0;
let mut max_y = 0;
let mut sorted_nodes: Vec<&Node> = graph.nodes.values().collect();
sorted_nodes.sort_by(|a, b| a.id.cmp(&b.id));
for node in &sorted_nodes {
max_x = max_x.max(node.x + node.width);
max_y = max_y.max(node.y + node.height);
}
for sg in &graph.subgraphs {
max_x = max_x.max(sg.x + sg.width);
max_y = max_y.max(sg.y + sg.height);
}
let mut grid = Grid::new(max_x + 2, max_y + 2);
for sg in &graph.subgraphs {
draw_subgraph(&mut grid, sg, chars);
protect_subgraph_borders(&mut grid, sg);
}
for node in &sorted_nodes {
draw_node(&mut grid, node, chars);
}
let path_grid = build_path_grid(graph, grid.width, grid.height);
let mut dropped_labels: Vec<edges::DroppedLabel> = Vec::new();
let mut next_marker: usize = 1;
for edge in &graph.edges {
if let (Some(from), Some(to)) = (graph.nodes.get(&edge.from), graph.nodes.get(&edge.to)) {
draw_edge(
&mut grid,
&path_grid,
from,
to,
edge,
chars,
graph.direction,
options.ascii,
&mut dropped_labels,
&mut next_marker,
);
}
}
let output = grid.to_string();
let output = if let Some(max_width) = options.max_width {
output
.lines()
.map(|line| {
let char_count = line.chars().count();
if char_count > max_width {
let mut truncated: String =
line.chars().take(max_width.saturating_sub(1)).collect();
truncated.push('…');
truncated
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
} else {
output
};
if !dropped_labels.is_empty() {
let mut result = output;
result.push_str("\nLabels:");
for dl in &dropped_labels {
result.push_str(&format!("\n {} {}", dl.marker, dl.label));
warnings.push(DiagramWarning::LabelDropped {
marker: dl.marker.clone(),
edge_from: dl.from.clone(),
edge_to: dl.to.clone(),
label: dl.label.clone(),
});
}
result
} else {
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::compute_layout;
use crate::parser::parse_mermaid;
#[test]
fn test_render_lr() {
let mut graph = parse_mermaid("flowchart LR\nA[Start] --> B[End]").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Start"));
assert!(output.contains("End"));
assert!(output.contains("▶"));
}
#[test]
fn test_render_tb() {
let mut graph = parse_mermaid("flowchart TB\nA[Start] --> B[End]").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Start"));
assert!(output.contains("End"));
assert!(output.contains("▼"));
}
#[test]
fn test_render_ascii() {
let mut graph = parse_mermaid("flowchart LR\nA --> B").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(
&graph,
&RenderOptions {
ascii: true,
..Default::default()
},
&mut warnings,
);
assert!(output.contains("+---+"));
assert!(output.contains(">"));
assert!(!output.contains("┌"));
}
#[test]
fn test_render_rl() {
let mut graph = parse_mermaid("flowchart RL\nA --> B").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("◀"));
}
#[test]
fn test_render_bt() {
let mut graph = parse_mermaid("flowchart BT\nA --> B").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("▲"));
}
#[test]
fn test_render_rounded() {
let mut graph = parse_mermaid("flowchart LR\nA(Rounded)").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Rounded"));
assert!(output.contains("╭")); }
#[test]
fn test_render_circle() {
let mut graph = parse_mermaid("flowchart LR\nA((Circle))").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Circle"));
assert!(output.contains("(")); }
#[test]
fn test_render_diamond() {
let mut graph = parse_mermaid("flowchart LR\nA{Decision}").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Decision"));
assert!(output.contains("<")); }
#[test]
fn test_render_cylinder() {
let mut graph = parse_mermaid("flowchart LR\nDB[(Database)]").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(&graph, &RenderOptions::default(), &mut warnings);
assert!(output.contains("Database"));
}
#[test]
fn test_render_max_width() {
let mut graph = parse_mermaid("flowchart LR\nA[Start] --> B[End]").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(
&graph,
&RenderOptions {
max_width: Some(15),
..Default::default()
},
&mut warnings,
);
for line in output.lines() {
assert!(
line.chars().count() <= 15,
"Line exceeds max_width: {} chars",
line.chars().count()
);
}
assert!(output.contains('…'));
}
#[test]
fn test_render_max_width_no_truncation() {
let mut graph = parse_mermaid("flowchart LR\nA --> B").unwrap();
compute_layout(&mut graph);
let mut warnings = Vec::new();
let output = render_graph(
&graph,
&RenderOptions {
max_width: Some(100), ..Default::default()
},
&mut warnings,
);
assert!(!output.contains('…'));
}
#[test]
fn test_diagonal_arrow_chars_exist() {
use super::charset::{ASCII_CHARS, UNICODE_CHARS};
assert_eq!(UNICODE_CHARS.arr_dr, '◢');
assert_eq!(UNICODE_CHARS.arr_dl, '◣');
assert_eq!(UNICODE_CHARS.arr_ur, '◥');
assert_eq!(UNICODE_CHARS.arr_ul, '◤');
assert_eq!(ASCII_CHARS.arr_dr, '\\');
assert_eq!(ASCII_CHARS.arr_dl, '/');
assert_eq!(ASCII_CHARS.arr_ur, '/');
assert_eq!(ASCII_CHARS.arr_ul, '\\');
}
#[test]
fn test_get_arrow_for_direction() {
use super::charset::UNICODE_CHARS;
use super::edges::get_arrow_for_direction;
use crate::pathfinding::Pos;
assert_eq!(
get_arrow_for_direction(Pos::new(0, 0), Pos::new(1, 0), '?', &UNICODE_CHARS),
'▶'
);
assert_eq!(
get_arrow_for_direction(Pos::new(1, 0), Pos::new(0, 0), '?', &UNICODE_CHARS),
'◀'
);
assert_eq!(
get_arrow_for_direction(Pos::new(0, 0), Pos::new(0, 1), '?', &UNICODE_CHARS),
'▼'
);
assert_eq!(
get_arrow_for_direction(Pos::new(0, 1), Pos::new(0, 0), '?', &UNICODE_CHARS),
'▲'
);
assert_eq!(
get_arrow_for_direction(Pos::new(0, 0), Pos::new(1, 1), '?', &UNICODE_CHARS),
'◢'
);
assert_eq!(
get_arrow_for_direction(Pos::new(1, 0), Pos::new(0, 1), '?', &UNICODE_CHARS),
'◣'
);
assert_eq!(
get_arrow_for_direction(Pos::new(0, 1), Pos::new(1, 0), '?', &UNICODE_CHARS),
'◥'
);
assert_eq!(
get_arrow_for_direction(Pos::new(1, 1), Pos::new(0, 0), '?', &UNICODE_CHARS),
'◤'
);
}
}