use super::MermaidError;
use crate::flowchart::{Direction, FlowEdge, FlowNode, Flowchart, Subgraph};
use crate::model::Shape;
pub(super) fn parse(stmts: &[(usize, String)]) -> Result<Flowchart, MermaidError> {
let mut fc = Flowchart {
direction: Direction::Tb,
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 skip_note = false;
let mut skip_acc = false;
for (_lineno, raw) in stmts.iter().skip(1) {
let stmt = raw.trim();
if stmt.is_empty() {
continue;
}
let low = stmt.to_ascii_lowercase();
if skip_note {
if low == "end note" {
skip_note = false;
}
continue;
}
if skip_acc {
if stmt.contains('}') {
skip_acc = false;
}
continue;
}
if low.starts_with("note ") || low == "note" {
if !stmt.contains(':') {
skip_note = true; }
continue;
}
if low.starts_with("accdescr") || low.starts_with("acctitle") {
if stmt.contains('{') && !stmt.contains('}') {
skip_acc = true;
}
continue;
}
if low.starts_with("classdef ")
|| low.starts_with("class ")
|| low.starts_with("style ")
|| low.starts_with("click ")
|| low.starts_with("scale ")
|| low == "{"
{
continue;
}
if low == "}" {
sub_stack.pop();
continue;
}
if low.starts_with("direction ") {
fc.direction = super::parse_direction(stmt[10..].trim());
continue;
}
if stmt.len() >= 2 && stmt.chars().all(|c| c == '-') {
continue;
}
if low == "state" || low.starts_with("state ") {
handle_decl(stmt[5..].trim(), &mut fc, &mut index, &mut sub_stack);
continue;
}
if let Some(pos) = stmt.find("-->") {
let lhs = stmt[..pos].trim();
let rest = stmt[pos + 3..].trim();
let (rhs, label) = match rest.split_once(':') {
Some((r, l)) => (r.trim(), l.trim().to_string()),
None => (rest, String::new()),
};
if lhs.is_empty() || rhs.is_empty() {
continue;
}
let src = resolve(lhs, true, &mut fc, &mut index, &sub_stack);
let dst = resolve(rhs, false, &mut fc, &mut index, &sub_stack);
fc.edges.push(FlowEdge {
src,
dst,
label,
dashed: false,
no_arrow: false,
});
continue;
}
if let Some((id, desc)) = stmt.split_once(':') {
let id = id.trim();
if !id.is_empty() && id != "[*]" {
touch(id, None, None, &mut fc, &mut index, &sub_stack);
set_label(&mut fc, &index, id, desc.trim());
}
continue;
}
if stmt != "[*]" {
touch(stmt, None, None, &mut fc, &mut index, &sub_stack);
}
}
Ok(fc)
}
fn resolve(
token: &str,
is_source: bool,
fc: &mut Flowchart,
index: &mut Vec<(String, usize)>,
sub_stack: &[usize],
) -> String {
if token == "[*]" {
let scope = sub_stack
.last()
.map(|i| fc.subgraphs[*i].id.clone())
.unwrap_or_default();
let id = if is_source {
format!("[*]s@{scope}")
} else {
format!("[*]e@{scope}")
};
touch(
&id,
Some(String::new()),
Some(Shape::Circle),
fc,
index,
sub_stack,
);
id
} else {
touch(token, None, None, fc, index, sub_stack)
}
}
fn handle_decl(
rest: &str,
fc: &mut Flowchart,
index: &mut Vec<(String, usize)>,
sub_stack: &mut Vec<usize>,
) {
let mut rest = rest.trim();
let opens = rest.ends_with('{');
if opens {
rest = rest[..rest.len() - 1].trim_end();
}
let mut shape = None;
if let Some(p) = rest.find("<<") {
let kind = &rest[p..];
shape = Some(if kind.contains("choice") {
Shape::Diamond
} else {
Shape::Box });
rest = rest[..p].trim_end();
}
let (id, label) = if let Some(apos) = rest.find(" as ") {
let desc = rest[..apos].trim().trim_matches('"').to_string();
(rest[apos + 4..].trim().to_string(), Some(desc))
} else {
(rest.trim().trim_matches('"').to_string(), None)
};
if id.is_empty() {
return;
}
if opens {
let title = label.unwrap_or_else(|| id.clone());
register_member(fc, sub_stack, &id);
let sub_idx = fc.subgraphs.len();
let parent = sub_stack.last().copied();
fc.subgraphs.push(Subgraph {
id: id.clone(),
title,
members: Vec::new(),
parent,
});
sub_stack.push(sub_idx);
} else {
touch(&id, label, shape, fc, index, sub_stack);
}
}
fn touch(
id: &str,
label: Option<String>,
shape: Option<Shape>,
fc: &mut Flowchart,
index: &mut Vec<(String, usize)>,
sub_stack: &[usize],
) -> String {
if let Some((_, idx)) = index.iter().find(|(i, _)| i == id) {
if let Some(l) = label {
fc.nodes[*idx].label = l;
}
if let Some(s) = shape {
fc.nodes[*idx].shape = s;
}
} else {
let idx = fc.nodes.len();
fc.nodes.push(FlowNode {
id: id.to_string(),
label: label.unwrap_or_else(|| id.to_string()),
shape: shape.unwrap_or(Shape::Box),
});
index.push((id.to_string(), idx));
}
register_member(fc, sub_stack, id);
id.to_string()
}
fn set_label(fc: &mut Flowchart, index: &[(String, usize)], id: &str, label: &str) {
if let Some((_, idx)) = index.iter().find(|(i, _)| i == id) {
fc.nodes[*idx].label = label.to_string();
}
}
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());
}
}
}
#[cfg(test)]
mod tests {
use super::super::parse_state;
use crate::flowchart::Direction;
#[test]
fn transitions_and_pseudo_states() {
let fc =
parse_state("stateDiagram-v2\n[*] --> Idle\nIdle --> Active: start\nActive --> [*]")
.unwrap();
assert!(fc.nodes.iter().any(|n| n.id == "Idle"));
assert!(fc.nodes.iter().any(|n| n.id == "Active"));
assert_eq!(
fc.nodes.iter().filter(|n| n.id.starts_with("[*]")).count(),
2
);
assert!(fc.edges.iter().any(|e| e.label == "start"));
}
#[test]
fn alias_direction_and_composite() {
let fc = parse_state(
"stateDiagram-v2\ndirection LR\nstate \"Long name\" as s1\nstate Outer {\n[*] --> Inner\n}",
)
.unwrap();
assert_eq!(fc.direction, Direction::Lr);
assert!(fc
.nodes
.iter()
.any(|n| n.id == "s1" && n.label == "Long name"));
let outer = fc.subgraphs.iter().find(|s| s.id == "Outer").unwrap();
assert!(outer.members.iter().any(|m| m == "Inner"));
}
}