mod lexer;
mod parser;
mod sequence;
use crate::flowchart::{Direction, FlowEdge, FlowNode, Flowchart, Subgraph};
use crate::model::Shape;
use parser::{parse_statement, Item};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MermaidError {
Empty,
Unsupported(String),
Syntax { line: usize, msg: String },
}
impl std::fmt::Display for MermaidError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MermaidError::Empty => write!(f, "empty Mermaid source"),
MermaidError::Unsupported(t) => write!(
f,
"unsupported Mermaid diagram type {t:?} (only flowchart `graph`/`flowchart` is supported)"
),
MermaidError::Syntax { line, msg } => write!(f, "line {line}: {msg}"),
}
}
}
impl std::error::Error for MermaidError {}
fn strip_comment(line: &str) -> &str {
match line.find("%%") {
Some(idx) => &line[..idx],
None => line,
}
}
pub fn parse(src: &str) -> Result<Flowchart, MermaidError> {
let mut stmts: Vec<(usize, String)> = Vec::new();
for (i, line) in src.lines().enumerate() {
for seg in strip_comment(line).split(';') {
let seg = seg.trim();
if !seg.is_empty() {
stmts.push((i + 1, seg.to_string()));
}
}
}
let header = stmts.first().ok_or(MermaidError::Empty)?;
let (kind, direction) = detect_type(&header.1)?;
if kind != "flowchart" {
return Err(MermaidError::Unsupported(kind));
}
let mut fc = Flowchart {
direction,
nodes: Vec::new(),
edges: Vec::new(),
subgraphs: Vec::new(),
};
let mut index: Vec<(String, usize)> = Vec::new();
let mut sub_stack: Vec<usize> = Vec::new();
let mut sub_auto = 0usize;
for (lineno, stmt) in stmts.iter().skip(1) {
handle_statement(
stmt,
*lineno,
&mut fc,
&mut index,
&mut sub_stack,
&mut sub_auto,
)?;
}
Ok(fc)
}
pub fn parse_sequence(src: &str) -> Result<crate::sequence::Sequence, MermaidError> {
let stmts: Vec<(usize, String)> = src
.lines()
.enumerate()
.map(|(i, line)| (i + 1, strip_comment(line).trim().to_string()))
.filter(|(_, s)| !s.is_empty())
.collect();
let header = stmts.first().ok_or(MermaidError::Empty)?;
let (kind, _dir) = detect_type(&header.1)?;
if kind != "sequenceDiagram" {
return Err(MermaidError::Unsupported(kind));
}
sequence::parse_sequence(&stmts[1..])
}
fn handle_statement(
stmt: &str,
lineno: usize,
fc: &mut Flowchart,
index: &mut Vec<(String, usize)>,
sub_stack: &mut Vec<usize>,
sub_auto: &mut usize,
) -> Result<(), MermaidError> {
let lower = stmt.to_ascii_lowercase();
if lower.starts_with("direction ") {
return Ok(());
}
if lower == "subgraph" || lower.starts_with("subgraph ") {
let rest = stmt[8..].trim();
let (id, title) = parse_subgraph_header(rest, sub_auto);
let sub_idx = fc.subgraphs.len();
fc.subgraphs.push(Subgraph {
id,
title,
members: Vec::new(),
});
sub_stack.push(sub_idx);
return Ok(());
}
if lower == "end" {
sub_stack.pop();
return Ok(());
}
let items = parse_statement(stmt).map_err(|msg| MermaidError::Syntax { line: lineno, msg })?;
let mut prev: Option<String> = None;
let mut pending_edge: Option<lexer::EdgeTok> = None;
for item in &items {
match item {
Item::Node(n) => {
touch_node(fc, index, n);
register_member(fc, sub_stack, &n.id);
if let (Some(src), Some(op)) = (prev.take(), pending_edge.take()) {
fc.edges.push(FlowEdge {
src,
dst: n.id.clone(),
label: op.label.clone(),
dashed: op.dashed,
no_arrow: op.no_arrow,
});
}
prev = Some(n.id.clone());
}
Item::Edge(op) => {
pending_edge = Some(op.clone());
}
}
}
Ok(())
}
fn touch_node(fc: &mut Flowchart, index: &mut Vec<(String, usize)>, n: &parser::ParsedNode) {
if let Some((_, idx)) = index.iter().find(|(id, _)| id == &n.id) {
let node = &mut fc.nodes[*idx];
if let Some(label) = &n.label {
node.label = label.clone();
}
if let Some(shape) = n.shape {
node.shape = shape;
}
} else {
let idx = fc.nodes.len();
fc.nodes.push(FlowNode {
id: n.id.clone(),
label: n.label.clone().unwrap_or_else(|| n.id.clone()),
shape: n.shape.unwrap_or(Shape::Box),
});
index.push((n.id.clone(), idx));
}
}
fn register_member(fc: &mut Flowchart, sub_stack: &[usize], id: &str) {
if let Some(&sub_idx) = sub_stack.last() {
let members = &mut fc.subgraphs[sub_idx].members;
if !members.iter().any(|m| m == id) {
members.push(id.to_string());
}
}
}
fn parse_subgraph_header(rest: &str, auto: &mut usize) -> (String, String) {
if rest.is_empty() {
*auto += 1;
return (format!("sub{auto}"), String::new());
}
if let Some(open) = rest.find('[') {
if rest.ends_with(']') {
let id = rest[..open].trim();
let title = rest[open + 1..rest.len() - 1].trim().trim_matches('"');
let id = if id.is_empty() {
*auto += 1;
format!("sub{auto}")
} else {
id.to_string()
};
return (id, title.to_string());
}
}
if rest.split_whitespace().count() == 1 {
(rest.to_string(), rest.to_string())
} else {
*auto += 1;
(format!("sub{auto}"), rest.trim_matches('"').to_string())
}
}
fn detect_type(header: &str) -> Result<(String, Direction), MermaidError> {
let mut words = header.split_whitespace();
let first = words.next().ok_or(MermaidError::Empty)?;
let kw = first.trim_end_matches(|c: char| !c.is_ascii_alphabetic());
let lower = kw.to_ascii_lowercase();
if lower == "graph" || lower == "flowchart" {
let dir = words.next().map(parse_direction).unwrap_or(Direction::Tb);
Ok(("flowchart".to_string(), dir))
} else {
Ok((kw.to_string(), Direction::Tb))
}
}
fn parse_direction(tok: &str) -> Direction {
match tok.to_ascii_uppercase().as_str() {
"LR" => Direction::Lr,
"RL" => Direction::Rl,
"BT" => Direction::Bt,
_ => Direction::Tb, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatch_unsupported() {
assert_eq!(
parse("sequenceDiagram\n A->>B: hi").unwrap_err(),
MermaidError::Unsupported("sequenceDiagram".to_string())
);
}
#[test]
fn header_direction() {
assert_eq!(
parse("flowchart LR\nA-->B").unwrap().direction,
Direction::Lr
);
assert_eq!(parse("graph TD\nA-->B").unwrap().direction, Direction::Tb);
assert_eq!(parse("graph\nA-->B").unwrap().direction, Direction::Tb);
}
#[test]
fn nodes_edges_and_dedup() {
let fc = parse("graph TD\nA[Start]-->B\nB-->C\nA-->C").unwrap();
assert_eq!(fc.nodes.len(), 3);
assert_eq!(fc.edges.len(), 3);
assert_eq!(fc.nodes[0].label, "Start");
assert_eq!(fc.nodes[1].label, "B");
}
#[test]
fn subgraph_membership() {
let fc = parse("flowchart TB\nsubgraph G [Group]\nA-->B\nend\nB-->C").unwrap();
assert_eq!(fc.subgraphs.len(), 1);
assert_eq!(fc.subgraphs[0].title, "Group");
assert_eq!(fc.subgraphs[0].members, ["A", "B"]);
}
#[test]
fn semicolon_separated() {
let fc = parse("graph LR; A-->B; B-->C").unwrap();
assert_eq!(fc.nodes.len(), 3);
assert_eq!(fc.edges.len(), 2);
}
#[test]
fn comments_skipped() {
let fc = parse("graph TD\n%% a comment\nA-->B %% trailing\n").unwrap();
assert_eq!(fc.edges.len(), 1);
}
#[test]
fn empty_is_error() {
assert_eq!(
parse(" \n %% only a comment\n").unwrap_err(),
MermaidError::Empty
);
}
}