use std::collections::BTreeMap;
use std::fmt;
use crate::model::ObjectRef;
pub mod diagram;
pub mod flowchart;
pub mod graph_paint;
pub mod lower;
pub mod pipeline;
pub mod scene;
pub mod sequence;
#[cfg(test)]
mod test_utils;
mod text;
pub mod track_paint;
pub use track_paint::{
paint_track_content_box, track_content_box_height, TRACK_BOX_HEIGHT_NO_NOTES,
TRACK_BOX_HEIGHT_WITH_NOTES, TRACK_MIN_BOX_INNER_WIDTH,
};
pub mod walkthrough;
pub use diagram::{render_diagram_unicode, render_diagram_unicode_annotated, DiagramRenderError};
pub use flowchart::{
render_flowchart_unicode, render_flowchart_unicode_annotated, FlowchartRenderError,
};
pub use graph_paint::{
graph_cap_glyph, graph_node_attach_mid_y, graph_node_box_height, graph_node_inner_rows,
graph_node_preferred_inner_width, paint_graph_node_box,
};
pub use lower::{
lower_class, lower_diagram_ast, lower_er, lower_flowchart, lower_gantt, lower_sequence,
};
pub use pipeline::{
render_ast_unicode_annotated_with_options, render_ast_unicode_with_options,
render_class_unicode_with_options, render_er_unicode_with_options,
render_graph_unicode_annotated_with_options, render_graph_unicode_with_options,
render_scene_unicode_annotated_with_options, render_scene_unicode_with_options,
render_track_unicode_annotated_with_options, render_track_unicode_with_options,
PipelineRenderError,
};
pub use scene::{
CapKind, EdgeStroke, GraphCompartment, GraphEdge, GraphModel, GraphNode, RenderScene,
TrackModel, TrackSpanStyle,
};
pub use sequence::{
render_sequence_unicode, render_sequence_unicode_annotated, SequenceRenderError,
};
pub use walkthrough::{render_walkthrough_unicode, WalkthroughRenderError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RenderOptions {
pub show_notes: bool,
pub prefix_object_labels: bool,
pub flowchart_extra_col_gap: usize,
}
pub type LineSpan = (usize, usize, usize);
pub type HighlightIndex = BTreeMap<ObjectRef, Vec<LineSpan>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnotatedRender {
pub text: String,
pub highlight_index: HighlightIndex,
}
pub(crate) fn clamp_highlight_index_to_text(highlight_index: &mut HighlightIndex, text: &str) {
let lines = text.split('\n').collect::<Vec<_>>();
let mut line_lens = Vec::<usize>::with_capacity(lines.len());
for line in &lines {
line_lens.push(text::text_len(line));
}
highlight_index.retain(|_, spans| {
spans.retain_mut(|span| {
let (y, x0, x1) = span;
let len = match line_lens.get(*y) {
Some(len) => *len,
None => return false,
};
if len == 0 {
return false;
}
if *x0 >= len {
return false;
}
let max_x = len - 1;
if *x1 > max_x {
*x1 = max_x;
}
*x0 <= *x1
});
!spans.is_empty()
});
}
pub const UNICODE_BOX_HORIZONTAL: char = '─';
pub const UNICODE_BOX_VERTICAL: char = '│';
pub const UNICODE_BOX_TOP_LEFT: char = '┌';
pub const UNICODE_BOX_TOP_RIGHT: char = '┐';
pub const UNICODE_BOX_BOTTOM_LEFT: char = '└';
pub const UNICODE_BOX_BOTTOM_RIGHT: char = '┘';
pub const UNICODE_BOX_TEE_RIGHT: char = '├';
pub const UNICODE_BOX_TEE_LEFT: char = '┤';
pub const UNICODE_BOX_TEE_DOWN: char = '┬';
pub const UNICODE_BOX_TEE_UP: char = '┴';
pub const UNICODE_BOX_CROSS: char = '┼';
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BoxEdges(u8);
impl BoxEdges {
const NONE: Self = Self(0);
const LEFT: Self = Self(1 << 0);
const RIGHT: Self = Self(1 << 1);
const UP: Self = Self(1 << 2);
const DOWN: Self = Self(1 << 3);
fn is_empty(self) -> bool {
self.0 == 0
}
fn contains(self, other: Self) -> bool {
(self.0 & other.0) != 0
}
fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
fn box_edges_from_char(ch: char) -> Option<BoxEdges> {
match ch {
UNICODE_BOX_HORIZONTAL => Some(BoxEdges::LEFT.union(BoxEdges::RIGHT)),
UNICODE_BOX_VERTICAL => Some(BoxEdges::UP.union(BoxEdges::DOWN)),
UNICODE_BOX_TOP_LEFT => Some(BoxEdges::RIGHT.union(BoxEdges::DOWN)),
UNICODE_BOX_TOP_RIGHT => Some(BoxEdges::LEFT.union(BoxEdges::DOWN)),
UNICODE_BOX_BOTTOM_LEFT => Some(BoxEdges::RIGHT.union(BoxEdges::UP)),
UNICODE_BOX_BOTTOM_RIGHT => Some(BoxEdges::LEFT.union(BoxEdges::UP)),
UNICODE_BOX_TEE_RIGHT => Some(BoxEdges::UP.union(BoxEdges::DOWN).union(BoxEdges::RIGHT)),
UNICODE_BOX_TEE_LEFT => Some(BoxEdges::UP.union(BoxEdges::DOWN).union(BoxEdges::LEFT)),
UNICODE_BOX_TEE_DOWN => Some(BoxEdges::LEFT.union(BoxEdges::RIGHT).union(BoxEdges::DOWN)),
UNICODE_BOX_TEE_UP => Some(BoxEdges::LEFT.union(BoxEdges::RIGHT).union(BoxEdges::UP)),
UNICODE_BOX_CROSS => {
Some(BoxEdges::LEFT.union(BoxEdges::RIGHT).union(BoxEdges::UP).union(BoxEdges::DOWN))
}
_ => None,
}
}
fn box_char_from_edges(edges: BoxEdges) -> char {
match edges.0 {
0 => ' ',
1..=3 => UNICODE_BOX_HORIZONTAL,
4 | 8 | 12 => UNICODE_BOX_VERTICAL,
10 => UNICODE_BOX_TOP_LEFT,
9 => UNICODE_BOX_TOP_RIGHT,
6 => UNICODE_BOX_BOTTOM_LEFT,
5 => UNICODE_BOX_BOTTOM_RIGHT,
14 => UNICODE_BOX_TEE_RIGHT,
13 => UNICODE_BOX_TEE_LEFT,
11 => UNICODE_BOX_TEE_DOWN,
7 => UNICODE_BOX_TEE_UP,
15 => UNICODE_BOX_CROSS,
_ => UNICODE_BOX_CROSS,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Canvas {
width: usize,
height: usize,
cells: Vec<char>,
box_edges: Vec<BoxEdges>,
}
impl Canvas {
pub fn new(width: usize, height: usize) -> Result<Self, CanvasError> {
Self::new_filled(width, height, ' ')
}
pub fn new_filled(width: usize, height: usize, fill: char) -> Result<Self, CanvasError> {
let len = width.checked_mul(height).ok_or(CanvasError::AreaOverflow { width, height })?;
Ok(Self { width, height, cells: vec![fill; len], box_edges: vec![BoxEdges::NONE; len] })
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn in_bounds(&self, x: usize, y: usize) -> bool {
x < self.width && y < self.height
}
pub fn get(&self, x: usize, y: usize) -> Result<char, CanvasError> {
let idx = self.index_of(x, y)?;
Ok(self.render_at(x, y, idx))
}
pub(crate) fn has_box_vertical(&self, x: usize, y: usize) -> Result<bool, CanvasError> {
let idx = self.index_of(x, y)?;
let edges = self.box_edges[idx];
Ok(edges.contains(BoxEdges::UP) || edges.contains(BoxEdges::DOWN))
}
pub fn set(&mut self, x: usize, y: usize, ch: char) -> Result<(), CanvasError> {
let idx = self.index_of(x, y)?;
if let Some(edges) = box_edges_from_char(ch) {
self.box_edges[idx] = self.box_edges[idx].union(edges);
} else {
self.cells[idx] = ch;
self.box_edges[idx] = BoxEdges::NONE;
}
Ok(())
}
pub(crate) fn set_exact(&mut self, x: usize, y: usize, ch: char) -> Result<(), CanvasError> {
let idx = self.index_of(x, y)?;
self.cells[idx] = ch;
self.box_edges[idx] = box_edges_from_char(ch).unwrap_or(BoxEdges::NONE);
Ok(())
}
pub fn fill(&mut self, ch: char) {
self.cells.fill(ch);
self.box_edges.fill(BoxEdges::NONE);
}
pub fn write_str(&mut self, x: usize, y: usize, text: &str) -> Result<(), CanvasError> {
if y >= self.height {
return Err(CanvasError::OutOfBounds { x, y, width: self.width, height: self.height });
}
for (x, ch) in (x..).zip(text.chars()) {
if x >= self.width {
break;
}
self.set(x, y, ch)?;
}
Ok(())
}
pub fn draw_hline(&mut self, x0: usize, x1: usize, y: usize) -> Result<(), CanvasError> {
let (min_x, max_x) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
if y >= self.height {
return Err(CanvasError::OutOfBounds {
x: min_x,
y,
width: self.width,
height: self.height,
});
}
if max_x >= self.width {
return Err(CanvasError::OutOfBounds {
x: max_x,
y,
width: self.width,
height: self.height,
});
}
for x in min_x..=max_x {
self.set(x, y, UNICODE_BOX_HORIZONTAL)?;
}
Ok(())
}
pub fn draw_vline(&mut self, x: usize, y0: usize, y1: usize) -> Result<(), CanvasError> {
let (min_y, max_y) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
if x >= self.width {
return Err(CanvasError::OutOfBounds {
x,
y: min_y,
width: self.width,
height: self.height,
});
}
if max_y >= self.height {
return Err(CanvasError::OutOfBounds {
x,
y: max_y,
width: self.width,
height: self.height,
});
}
for y in min_y..=max_y {
self.set(x, y, UNICODE_BOX_VERTICAL)?;
}
Ok(())
}
pub fn draw_box(
&mut self,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
) -> Result<(), CanvasError> {
let (min_x, max_x) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
let (min_y, max_y) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
if max_x >= self.width {
return Err(CanvasError::OutOfBounds {
x: max_x,
y: min_y,
width: self.width,
height: self.height,
});
}
if max_y >= self.height {
return Err(CanvasError::OutOfBounds {
x: min_x,
y: max_y,
width: self.width,
height: self.height,
});
}
if min_x == max_x && min_y == max_y {
return self.set(min_x, min_y, UNICODE_BOX_CROSS);
}
if min_y == max_y {
return self.draw_hline(min_x, max_x, min_y);
}
if min_x == max_x {
return self.draw_vline(min_x, min_y, max_y);
}
for x in (min_x + 1)..max_x {
self.set(x, min_y, UNICODE_BOX_HORIZONTAL)?;
self.set(x, max_y, UNICODE_BOX_HORIZONTAL)?;
}
for y in (min_y + 1)..max_y {
self.set(min_x, y, UNICODE_BOX_VERTICAL)?;
self.set(max_x, y, UNICODE_BOX_VERTICAL)?;
}
self.set(min_x, min_y, UNICODE_BOX_TOP_LEFT)?;
self.set(max_x, min_y, UNICODE_BOX_TOP_RIGHT)?;
self.set(min_x, max_y, UNICODE_BOX_BOTTOM_LEFT)?;
self.set(max_x, max_y, UNICODE_BOX_BOTTOM_RIGHT)?;
Ok(())
}
fn index_of(&self, x: usize, y: usize) -> Result<usize, CanvasError> {
if !self.in_bounds(x, y) {
return Err(CanvasError::OutOfBounds { x, y, width: self.width, height: self.height });
}
Ok((y * self.width) + x)
}
fn render_at(&self, x: usize, y: usize, idx: usize) -> char {
let edges = self.box_edges[idx];
if edges.is_empty() {
return self.cells[idx];
}
let connected = self.connected_box_edges(x, y, edges);
let edges_for_render = if connected.is_empty() { edges } else { connected };
box_char_from_edges(edges_for_render)
}
fn connected_box_edges(&self, x: usize, y: usize, edges: BoxEdges) -> BoxEdges {
let mut connected = BoxEdges::NONE;
if edges.contains(BoxEdges::LEFT) && x > 0 {
let left_idx = (y * self.width) + (x - 1);
if self.box_edges[left_idx].contains(BoxEdges::RIGHT) {
connected = connected.union(BoxEdges::LEFT);
}
}
if edges.contains(BoxEdges::RIGHT) && (x + 1) < self.width {
let right_idx = (y * self.width) + (x + 1);
if self.box_edges[right_idx].contains(BoxEdges::LEFT) {
connected = connected.union(BoxEdges::RIGHT);
}
}
if edges.contains(BoxEdges::UP) && y > 0 {
let up_idx = ((y - 1) * self.width) + x;
if self.box_edges[up_idx].contains(BoxEdges::DOWN) {
connected = connected.union(BoxEdges::UP);
}
}
if edges.contains(BoxEdges::DOWN) && (y + 1) < self.height {
let down_idx = ((y + 1) * self.width) + x;
if self.box_edges[down_idx].contains(BoxEdges::UP) {
connected = connected.union(BoxEdges::DOWN);
}
}
connected
}
}
impl fmt::Display for Canvas {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::fmt::Write as _;
for y in 0..self.height {
for x in 0..self.width {
let idx = (y * self.width) + x;
let ch = self.render_at(x, y, idx);
f.write_char(ch)?;
}
if y + 1 < self.height {
f.write_char('\n')?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CanvasError {
AreaOverflow { width: usize, height: usize },
OutOfBounds { x: usize, y: usize, width: usize, height: usize },
}
impl fmt::Display for CanvasError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AreaOverflow { width, height } => {
write!(f, "canvas area overflow: {width}*{height}")
}
Self::OutOfBounds { x, y, width, height } => {
write!(f, "out of bounds: ({x},{y}) for {width}x{height} canvas")
}
}
}
}
impl std::error::Error for CanvasError {}
#[cfg(test)]
mod tests {
use super::{clamp_highlight_index_to_text, Canvas, CanvasError, HighlightIndex};
use crate::model::ObjectRef;
#[test]
fn set_and_get_in_bounds() {
let mut c = Canvas::new_filled(3, 2, '.').expect("canvas");
assert_eq!(c.get(1, 0).unwrap(), '.');
c.set(1, 0, 'X').unwrap();
assert_eq!(c.get(1, 0).unwrap(), 'X');
assert_eq!(c.to_string(), ".X.\n...");
}
#[test]
fn set_out_of_bounds_errors() {
let mut c = Canvas::new(2, 2).expect("canvas");
let err = c.set(2, 0, 'X').unwrap_err();
assert_eq!(err, CanvasError::OutOfBounds { x: 2, y: 0, width: 2, height: 2 });
}
#[test]
fn get_out_of_bounds_errors() {
let c = Canvas::new(2, 2).expect("canvas");
let err = c.get(0, 2).unwrap_err();
assert_eq!(err, CanvasError::OutOfBounds { x: 0, y: 2, width: 2, height: 2 });
}
#[test]
fn write_str_clips_at_right_edge() {
let mut c = Canvas::new_filled(4, 1, '.').expect("canvas");
c.write_str(2, 0, "abcdef").unwrap();
assert_eq!(c.to_string(), "..ab");
}
#[test]
fn rejects_area_overflow() {
let err = Canvas::new_filled(usize::MAX, 2, '.').unwrap_err();
assert_eq!(err, CanvasError::AreaOverflow { width: usize::MAX, height: 2 });
}
#[test]
fn clamp_highlight_index_to_text_clamps_right_edge_and_drops_invalid_spans() {
let mut highlight_index = HighlightIndex::new();
let kept_ref: ObjectRef = "d:d-clamp/seq/message/m:0001".parse().expect("keep object ref");
let dropped_ref: ObjectRef =
"d:d-clamp/seq/message/m:0002".parse().expect("drop object ref");
highlight_index.insert(
kept_ref.clone(),
vec![
(0, 1, 99), (0, 4, 4), (1, 0, 0), (2, 1, 9), (9, 0, 0), ],
);
highlight_index.insert(dropped_ref.clone(), vec![(1, 0, 0), (7, 0, 0)]);
clamp_highlight_index_to_text(&mut highlight_index, "abcd\n\nef");
assert_eq!(highlight_index.len(), 1, "expected only kept_ref to remain");
assert!(
!highlight_index.contains_key(&dropped_ref),
"expected object with only invalid spans to be removed"
);
assert_eq!(
highlight_index.remove(&kept_ref).expect("kept_ref spans"),
vec![(0, 1, 3), (2, 1, 1)],
"expected right-edge clamp and invalid-span pruning"
);
}
#[test]
fn clamp_highlight_index_to_text_produces_only_in_bounds_spans() {
let mut highlight_index = HighlightIndex::new();
let a_ref: ObjectRef = "d:d-clamp/flow/edge/e:a".parse().expect("a ref");
let b_ref: ObjectRef = "d:d-clamp/flow/edge/e:b".parse().expect("b ref");
highlight_index.insert(a_ref, vec![(0, 0, 5), (0, 2, 1), (2, 0, 0)]);
highlight_index.insert(b_ref, vec![(0, 1, 3), (3, 0, 0), (2, 1, 4)]);
let text = "abc\n\nwxyz";
clamp_highlight_index_to_text(&mut highlight_index, text);
let lines = text.split('\n').collect::<Vec<_>>();
for (object_ref, spans) in &highlight_index {
assert!(!spans.is_empty(), "clamp left empty span list for {object_ref}");
for (span_idx, (y, x0, x1)) in spans.iter().copied().enumerate() {
let line = lines.get(y).unwrap_or_else(|| {
panic!("clamp left out-of-bounds y={y} for {object_ref} span #{span_idx}")
});
let line_len = super::text::text_len(line);
assert!(
line_len > 0,
"clamp left span on empty line for {object_ref} span #{span_idx}: (y={y}, x0={x0}, x1={x1})"
);
assert!(
x0 <= x1,
"clamp left inverted span for {object_ref} span #{span_idx}: (y={y}, x0={x0}, x1={x1})"
);
assert!(
x1 < line_len,
"clamp left out-of-bounds x1 for {object_ref} span #{span_idx}: (y={y}, x0={x0}, x1={x1}, line_len={line_len})"
);
}
}
}
#[test]
fn draw_hline_draws_unicode_horizontal() {
let mut c = Canvas::new_filled(5, 3, '.').expect("canvas");
c.draw_hline(1, 3, 1).unwrap();
assert_eq!(c.to_string(), ".....\n.───.\n.....");
}
#[test]
fn draw_vline_draws_unicode_vertical() {
let mut c = Canvas::new_filled(5, 3, '.').expect("canvas");
c.draw_vline(2, 0, 2).unwrap();
assert_eq!(c.to_string(), "..│..\n..│..\n..│..");
}
#[test]
fn draw_box_draws_unicode_corners_and_edges() {
let mut c = Canvas::new_filled(6, 5, '.').expect("canvas");
c.draw_box(1, 1, 4, 3).unwrap();
assert_eq!(c.to_string(), "......\n.┌──┐.\n.│..│.\n.└──┘.\n......");
}
#[test]
fn draw_box_out_of_bounds_is_not_partial() {
let mut c = Canvas::new_filled(4, 3, '.').expect("canvas");
let err = c.draw_box(0, 0, 4, 2).unwrap_err();
assert_eq!(err, CanvasError::OutOfBounds { x: 4, y: 0, width: 4, height: 3 });
assert_eq!(c.to_string(), "....\n....\n....");
}
#[test]
fn intersects_hline_and_vline_as_cross_not_overwrite() {
let mut c = Canvas::new_filled(5, 5, '.').expect("canvas");
c.draw_hline(0, 4, 2).unwrap();
c.draw_vline(2, 0, 4).unwrap();
assert_eq!(c.to_string(), "..│..\n..│..\n──┼──\n..│..\n..│..");
}
#[test]
fn intersects_as_left_and_right_tees() {
let mut c = Canvas::new_filled(5, 5, '.').expect("canvas");
c.draw_vline(2, 0, 4).unwrap();
c.draw_hline(2, 4, 2).unwrap();
assert_eq!(c.to_string(), "..│..\n..│..\n..├──\n..│..\n..│..");
let mut c = Canvas::new_filled(5, 5, '.').expect("canvas");
c.draw_vline(2, 0, 4).unwrap();
c.draw_hline(0, 2, 2).unwrap();
assert_eq!(c.to_string(), "..│..\n..│..\n──┤..\n..│..\n..│..");
}
#[test]
fn intersects_as_top_and_bottom_tees() {
let mut c = Canvas::new_filled(5, 5, '.').expect("canvas");
c.draw_hline(0, 4, 2).unwrap();
c.draw_vline(2, 2, 4).unwrap();
assert_eq!(c.to_string(), ".....\n.....\n──┬──\n..│..\n..│..");
let mut c = Canvas::new_filled(5, 5, '.').expect("canvas");
c.draw_hline(0, 4, 2).unwrap();
c.draw_vline(2, 0, 2).unwrap();
assert_eq!(c.to_string(), "..│..\n..│..\n──┴──\n.....\n.....");
}
}