#![allow(dead_code)]
pub mod canvas;
pub mod flow;
pub mod sequence;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
Line,
Node,
Label,
Bright,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Run {
pub text: String,
pub role: Role,
}
impl Run {
pub fn new(text: impl Into<String>, role: Role) -> Run {
Run {
text: text.into(),
role,
}
}
}
pub type Row = Vec<Run>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Rendered {
pub rows: Vec<Row>,
pub width: usize,
}
impl Rendered {
pub fn new(rows: Vec<Row>) -> Rendered {
let width = rows
.iter()
.map(|r| {
r.iter()
.map(|run| crate::md::str_width(&run.text))
.sum::<usize>()
})
.max()
.unwrap_or(0);
Rendered { rows, width }
}
pub fn height(&self) -> usize {
self.rows.len()
}
pub fn text(&self) -> Vec<String> {
self.rows
.iter()
.map(|r| r.iter().map(|run| run.text.as_str()).collect())
.collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dir {
Lr,
Rl,
Td,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Flow(Dir),
Sequence,
}
pub fn is_mermaid(info: &str) -> bool {
info.trim()
.split(|c: char| c.is_whitespace() || c == '{')
.next()
.is_some_and(|w| w.eq_ignore_ascii_case("mermaid"))
}
pub fn kind_word(src: &str) -> Option<String> {
header(src).map(|(word, _)| word)
}
pub fn kind_of(src: &str) -> Option<Kind> {
let (word, rest) = header(src)?;
match word.to_ascii_lowercase().as_str() {
"flowchart" | "graph" => Some(Kind::Flow(direction(&rest))),
"sequencediagram" => Some(Kind::Sequence),
_ => None,
}
}
pub fn render(src: &str, width: usize) -> Option<Rendered> {
match kind_of(src)? {
Kind::Flow(dir) => flow::render(src, dir, width),
Kind::Sequence => sequence::render(src, width),
}
}
fn header(src: &str) -> Option<(String, String)> {
let line = src
.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with("%%"))?;
let line = line.trim_end_matches(';').trim();
let mut parts = line.splitn(2, char::is_whitespace);
let word = parts.next()?.trim_end_matches(';').to_string();
if word.is_empty() {
return None;
}
Some((word, parts.next().unwrap_or("").trim().to_string()))
}
fn direction(rest: &str) -> Dir {
let word = rest
.split(|c: char| c.is_whitespace() || c == ';')
.find(|w| !w.is_empty())
.unwrap_or("");
match word.to_ascii_uppercase().as_str() {
"LR" => Dir::Lr,
"RL" => Dir::Rl,
_ => Dir::Td,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fence_that_names_mermaid_is_a_diagram() {
assert!(is_mermaid("mermaid"));
assert!(is_mermaid("Mermaid"));
assert!(is_mermaid("mermaid {init: {}}"));
assert!(!is_mermaid("rust"));
assert!(!is_mermaid("mermaidjs"));
assert!(!is_mermaid(""));
}
#[test]
fn a_flowchart_header_names_its_direction() {
assert_eq!(kind_of("flowchart LR\nA --> B"), Some(Kind::Flow(Dir::Lr)));
assert_eq!(kind_of("graph RL;\nA --> B"), Some(Kind::Flow(Dir::Rl)));
assert_eq!(kind_of("flowchart TB\nA --> B"), Some(Kind::Flow(Dir::Td)));
assert_eq!(kind_of("graph TD\nA --> B"), Some(Kind::Flow(Dir::Td)));
assert_eq!(kind_of("flowchart\nA --> B"), Some(Kind::Flow(Dir::Td)));
assert_eq!(kind_of("flowchart BT\nA --> B"), Some(Kind::Flow(Dir::Td)));
}
#[test]
fn comments_and_blank_lines_above_the_header_are_skipped() {
let src = "\n%% drawn by hand\nsequenceDiagram\n A->>B: hi";
assert_eq!(kind_of(src), Some(Kind::Sequence));
assert_eq!(kind_word(src).as_deref(), Some("sequenceDiagram"));
}
#[test]
fn an_unsupported_diagram_kind_is_not_drawn() {
let src = "classDiagram\n Animal <|-- Duck";
assert_eq!(kind_of(src), None);
assert_eq!(render(src, 80), None);
assert_eq!(kind_word(src).as_deref(), Some("classDiagram"));
assert_eq!(kind_word(" \n\n"), None);
}
#[test]
fn a_diagram_measures_its_widest_row() {
let r = Rendered::new(vec![
vec![Run::new("──", Role::Line)],
vec![Run::new("│ ", Role::Line), Run::new("hi", Role::Node)],
]);
assert_eq!(r.width, 4);
assert_eq!(r.height(), 2);
assert_eq!(r.text(), vec!["──".to_string(), "│ hi".to_string()]);
}
}