use super::model::{
AutoNumber, BlockKind, Event, Head, Line, Message, Note, ParseError, Participant,
ParticipantBox, ParticipantKind, Placement, SectionKind, SequenceDiagram, Signal,
};
use crate::preview::mermaid::flowchart::preprocess::preprocess;
use crate::preview::mermaid::flowchart::text::decode_label;
const MAX_MESSAGES: usize = 500;
#[cfg(test)]
pub const MAX_MESSAGES_FOR_TESTS: usize = MAX_MESSAGES;
pub fn parse(src: &str) -> Result<SequenceDiagram, ParseError> {
let pre = preprocess(src);
let lines: Vec<&str> = pre.text.split('\n').collect();
let header = find_header(&lines)?;
let mut scanner = Scanner::new(pre.title);
scanner.line(&header.trailing, header.line_index + 1)?;
for (i, line) in lines.iter().enumerate().skip(header.line_index + 1) {
scanner.line(line, i + 1)?;
}
scanner.finish()
}
pub fn is_sequence_diagram(src: &str) -> bool {
let pre = preprocess(src);
let lines: Vec<&str> = pre.text.split('\n').collect();
find_header(&lines).is_ok()
}
struct Header {
line_index: usize,
trailing: String,
}
fn find_header(lines: &[&str]) -> Result<Header, ParseError> {
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
continue;
}
let t = line.trim_start();
if starts_ci(t, "sequenceDiagram") {
let rest = &t[15..];
if !rest.chars().next().is_some_and(is_id_char) {
return Ok(Header {
line_index: i,
trailing: rest.to_string(),
});
}
}
let header: String = t
.chars()
.take_while(|c| !c.is_whitespace())
.take(40)
.collect();
return Err(ParseError::NotASequenceDiagram { header });
}
Err(ParseError::Empty)
}
fn is_id_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn starts_ci(s: &str, kw: &str) -> bool {
s.len() >= kw.len() && s.is_char_boundary(kw.len()) && s[..kw.len()].eq_ignore_ascii_case(kw)
}
fn entity_at(chars: &[char], i: usize) -> Option<usize> {
if chars.get(i) != Some(&'#') {
return None;
}
let mut p = i + 1;
let digits = chars.get(p).is_some_and(|c| c.is_ascii_digit());
while chars.get(p).is_some_and(|c| {
if digits {
c.is_ascii_digit()
} else {
c.is_ascii_alphabetic()
}
}) {
p += 1;
}
if p == i + 1 || chars.get(p) != Some(&';') {
return None;
}
Some(p + 1)
}
fn split_statements(line: &str) -> Vec<String> {
let chars: Vec<char> = line.chars().collect();
let mut out: Vec<String> = Vec::new();
let mut current = String::new();
let mut i = 0;
while i < chars.len() {
if let Some(end) = entity_at(&chars, i) {
current.extend(&chars[i..end]);
i = end;
continue;
}
match chars[i] {
'#' => break,
';' => {
out.push(std::mem::take(&mut current));
i += 1;
}
c => {
current.push(c);
i += 1;
}
}
}
out.push(current);
out
}
fn head_word(stmt: &str) -> (String, &str) {
let t = stmt.trim_start();
let end = t.find(char::is_whitespace).unwrap_or(t.len());
(t[..end].to_ascii_lowercase(), t[end..].trim_start())
}
fn strip_wrap(s: &str) -> &str {
let t = s.strip_prefix(':').unwrap_or(s);
for prefix in ["nowrap:", "wrap:"] {
if starts_ci(t, prefix) {
return t[prefix.len()..].trim_start();
}
}
s
}
const ARROWS: &[(&str, Line, Head, Head)] = &[
("<<-->>", Line::Dotted, Head::Arrow, Head::Arrow),
("<<->>", Line::Solid, Head::Arrow, Head::Arrow),
("--|\\", Line::Dotted, Head::None, Head::Arrow),
("--|/", Line::Dotted, Head::None, Head::Arrow),
("--\\\\", Line::Dotted, Head::None, Head::Arrow),
("--//", Line::Dotted, Head::None, Head::Arrow),
("/|--", Line::Dotted, Head::Arrow, Head::None),
("\\|--", Line::Dotted, Head::Arrow, Head::None),
("//--", Line::Dotted, Head::Arrow, Head::None),
("\\\\--", Line::Dotted, Head::Arrow, Head::None),
("-->>", Line::Dotted, Head::None, Head::Arrow),
("-->", Line::Dotted, Head::None, Head::None),
("--x", Line::Dotted, Head::None, Head::Cross),
("--)", Line::Dotted, Head::None, Head::Async),
("-|\\", Line::Solid, Head::None, Head::Arrow),
("-|/", Line::Solid, Head::None, Head::Arrow),
("-\\\\", Line::Solid, Head::None, Head::Arrow),
("-//", Line::Solid, Head::None, Head::Arrow),
("/|-", Line::Solid, Head::Arrow, Head::None),
("\\|-", Line::Solid, Head::Arrow, Head::None),
("//-", Line::Solid, Head::Arrow, Head::None),
("\\\\-", Line::Solid, Head::Arrow, Head::None),
("->>", Line::Solid, Head::None, Head::Arrow),
("->", Line::Solid, Head::None, Head::None),
("-x", Line::Solid, Head::None, Head::Cross),
("-)", Line::Solid, Head::None, Head::Async),
];
fn find_arrow(stmt: &str) -> Option<(usize, usize, Signal)> {
let bytes = stmt.as_bytes();
for i in 0..bytes.len() {
if !stmt.is_char_boundary(i) {
continue;
}
for (spelling, line, start, end) in ARROWS {
if stmt[i..].starts_with(spelling) {
if stmt[..i].contains(':') {
return None;
}
return Some((
i,
i + spelling.len(),
Signal {
line: *line,
start: *start,
end: *end,
},
));
}
}
}
None
}
struct Open {
block: Option<BlockKind>,
keyword: &'static str,
line: usize,
}
struct Scanner {
diagram: SequenceDiagram,
open: Vec<Open>,
active: Vec<(String, i32)>,
last_created: Option<String>,
last_destroyed: Option<String>,
in_acc_descr: bool,
acc_descr_buf: String,
messages: usize,
}
impl Scanner {
fn new(front_matter_title: Option<String>) -> Scanner {
Scanner {
diagram: SequenceDiagram {
title: front_matter_title,
..SequenceDiagram::default()
},
open: Vec::new(),
active: Vec::new(),
last_created: None,
last_destroyed: None,
in_acc_descr: false,
acc_descr_buf: String::new(),
messages: 0,
}
}
fn line(&mut self, raw: &str, number: usize) -> Result<(), ParseError> {
if self.in_acc_descr {
match raw.find('}') {
Some(i) => {
self.acc_descr_buf.push_str(&raw[..i]);
self.in_acc_descr = false;
let text = std::mem::take(&mut self.acc_descr_buf);
self.diagram.acc_descr = Some(text.trim().to_string());
}
None => {
self.acc_descr_buf.push_str(raw);
self.acc_descr_buf.push('\n');
}
}
return Ok(());
}
for stmt in split_statements(raw) {
self.statement(&stmt, number)?;
}
Ok(())
}
fn statement(&mut self, stmt: &str, line: usize) -> Result<(), ParseError> {
let t = stmt.trim();
if t.is_empty() {
return Ok(());
}
let (word, rest) = head_word(t);
match word.as_str() {
"participant" => self.participant(rest, ParticipantKind::Participant, line, false),
"actor" => self.participant(rest, ParticipantKind::Actor, line, false),
"create" => {
let (w2, rest2) = head_word(rest);
match w2.as_str() {
"participant" => {
self.participant(rest2, ParticipantKind::Participant, line, true)
}
"actor" => self.participant(rest2, ParticipantKind::Actor, line, true),
_ => self.participant(rest, ParticipantKind::Participant, line, true),
}
}
"destroy" => self.destroy(rest, line),
"box" => self.open_box(rest, line),
"end" => self.close(line),
"loop" => self.open_block(BlockKind::Loop, "loop", rest, line),
"rect" => {
let (_color, title) = split_box_data(strip_wrap(rest));
self.open_block(BlockKind::Rect, "rect", &title, line)
}
"opt" => self.open_block(BlockKind::Opt, "opt", rest, line),
"alt" => self.open_block(BlockKind::Alt, "alt", rest, line),
"par" => self.open_block(BlockKind::Par, "par", rest, line),
"par_over" => self.open_block(BlockKind::ParOver, "par_over", rest, line),
"critical" => self.open_block(BlockKind::Critical, "critical", rest, line),
"break" => self.open_block(BlockKind::Break, "break", rest, line),
"else" => self.section(SectionKind::Else, rest, line),
"and" => self.section(SectionKind::And, rest, line),
"option" => self.section(SectionKind::Option, rest, line),
"note" => self.note(rest, line),
"links" | "link" | "properties" | "details" => {
let (name, _payload) = split_once_colon(rest);
let name = name.trim();
if name.is_empty() {
return Err(ParseError::MissingActor {
keyword: "link",
line,
});
}
self.diagram.ensure(name, ParticipantKind::Participant);
Ok(())
}
"activate" => self.activate(rest, line),
"deactivate" => self.deactivate(rest, line),
"autonumber" => self.autonumber(rest),
"title" => {
self.diagram.title = non_empty(decode_label(rest));
Ok(())
}
_ => {
if let Some(v) = after_colon_keyword(t, "title") {
self.diagram.title = non_empty(decode_label(&v));
return Ok(());
}
if let Some(v) = after_colon_keyword(t, "accTitle") {
self.diagram.acc_title = non_empty(decode_label(&v));
return Ok(());
}
if starts_ci(t, "accDescr") {
let after = t[8..].trim_start();
if let Some(body) = after.strip_prefix('{') {
match body.find('}') {
Some(i) => {
self.diagram.acc_descr = non_empty(body[..i].trim().to_string());
}
None => {
self.in_acc_descr = true;
self.acc_descr_buf = format!("{body}\n");
}
}
return Ok(());
}
if let Some(v) = after.strip_prefix(':') {
self.diagram.acc_descr = non_empty(decode_label(v));
return Ok(());
}
}
self.message(t, line)
}
}
}
fn participant(
&mut self,
rest: &str,
kind: ParticipantKind,
line: usize,
created: bool,
) -> Result<(), ParseError> {
let (head, alias) = split_as(rest);
let (id, config) = split_config(head);
let id = id.trim();
if id.is_empty() {
return Err(ParseError::MissingActor {
keyword: "participant",
line,
});
}
if created && self.diagram.has(id) {
return Err(ParseError::DuplicateCreate {
name: id.to_string(),
line,
});
}
let mut kind = kind;
let mut label: Option<String> = alias.map(|a| decode_label(strip_wrap(a)));
if let Some(config) = config {
if config_value(config, "type").as_deref() == Some("actor") {
kind = ParticipantKind::Actor;
}
if label.is_none() {
label = config_value(config, "alias").map(|a| decode_label(&a));
}
}
let existed = self.diagram.has(id);
let i = self.diagram.ensure(id, kind);
let p = &mut self.diagram.participants[i];
if let Some(label) = label {
p.label = label;
}
if kind == ParticipantKind::Actor {
p.kind = ParticipantKind::Actor;
}
if created && !existed {
p.created_at = Some(self.diagram.events.len());
self.diagram.events.push(Event::Create(id.to_string()));
self.last_created = Some(id.to_string());
}
if let Some(open) = self.open.last() {
if open.block.is_none() {
let box_id = self
.diagram
.boxes
.last()
.map(|b| b.id.clone())
.unwrap_or_default();
if let Some(b) = self.diagram.boxes.last_mut() {
if !b.members.iter().any(|m| m == id) {
b.members.push(id.to_string());
}
}
let p = &mut self.diagram.participants[i];
if p.box_id.is_none() {
p.box_id = Some(box_id);
}
}
}
Ok(())
}
fn destroy(&mut self, rest: &str, line: usize) -> Result<(), ParseError> {
let id = rest.trim();
if id.is_empty() {
return Err(ParseError::MissingActor {
keyword: "destroy",
line,
});
}
let at = self.diagram.events.len();
if let Some(p) = self.diagram.participant_mut(id) {
p.destroyed_at = Some(at);
}
self.diagram.events.push(Event::Destroy(id.to_string()));
self.last_destroyed = Some(id.to_string());
Ok(())
}
fn open_box(&mut self, rest: &str, line: usize) -> Result<(), ParseError> {
let (color, title) = split_box_data(strip_wrap(rest));
let id = format!("box{}", self.diagram.boxes.len());
self.diagram.boxes.push(ParticipantBox {
id,
title: decode_label(&title),
color,
members: Vec::new(),
});
self.open.push(Open {
block: None,
keyword: "box",
line,
});
Ok(())
}
fn open_block(
&mut self,
kind: BlockKind,
keyword: &'static str,
rest: &str,
line: usize,
) -> Result<(), ParseError> {
self.diagram.events.push(Event::BlockStart {
kind,
title: decode_label(strip_wrap(rest)),
});
self.open.push(Open {
block: Some(kind),
keyword,
line,
});
Ok(())
}
fn section(&mut self, kind: SectionKind, rest: &str, line: usize) -> Result<(), ParseError> {
if !self.open.iter().any(|o| o.block.is_some()) {
return Err(ParseError::SectionOutsideBlock {
keyword: kind.keyword(),
line,
});
}
self.diagram.events.push(Event::Section {
kind,
title: decode_label(strip_wrap(rest)),
});
Ok(())
}
fn close(&mut self, line: usize) -> Result<(), ParseError> {
let Some(open) = self.open.pop() else {
return Err(ParseError::UnmatchedEnd { line });
};
if open.block.is_some() {
self.diagram.events.push(Event::BlockEnd);
}
Ok(())
}
fn note(&mut self, rest: &str, line: usize) -> Result<(), ParseError> {
let (placement, after) = if starts_ci(rest, "left of") {
(Placement::LeftOf, rest[7..].trim_start())
} else if starts_ci(rest, "right of") {
(Placement::RightOf, rest[8..].trim_start())
} else if starts_ci(rest, "over") {
(Placement::Over, rest[4..].trim_start())
} else {
return Err(ParseError::MissingActor {
keyword: "note",
line,
});
};
let (names, text) = split_once_colon(after);
let actors: Vec<String> = names
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if actors.is_empty() {
return Err(ParseError::MissingActor {
keyword: "note",
line,
});
}
for a in &actors {
self.diagram.ensure(a, ParticipantKind::Participant);
}
let actors = actors.into_iter().take(2).collect();
self.diagram.events.push(Event::Note(Note {
actors,
placement,
text: decode_label(strip_wrap(&text)),
}));
Ok(())
}
fn activate(&mut self, rest: &str, line: usize) -> Result<(), ParseError> {
let id = rest.trim();
if id.is_empty() {
return Err(ParseError::MissingActor {
keyword: "activate",
line,
});
}
self.push_activate(id);
Ok(())
}
fn deactivate(&mut self, rest: &str, line: usize) -> Result<(), ParseError> {
let id = rest.trim();
if id.is_empty() {
return Err(ParseError::MissingActor {
keyword: "deactivate",
line,
});
}
self.push_deactivate(id, line)
}
fn push_activate(&mut self, id: &str) {
match self.active.iter_mut().find(|(n, _)| n == id) {
Some((_, n)) => *n += 1,
None => self.active.push((id.to_string(), 1)),
}
self.diagram.events.push(Event::Activate(id.to_string()));
}
fn push_deactivate(&mut self, id: &str, line: usize) -> Result<(), ParseError> {
let count = self
.active
.iter_mut()
.find(|(n, _)| n == id)
.map(|(_, n)| n)
.filter(|n| **n > 0);
let Some(count) = count else {
return Err(ParseError::NotActive {
name: id.to_string(),
line,
});
};
*count -= 1;
self.diagram.events.push(Event::Deactivate(id.to_string()));
Ok(())
}
fn autonumber(&mut self, rest: &str) -> Result<(), ParseError> {
let words: Vec<&str> = rest.split_whitespace().collect();
let cmd = match words.first().map(|w| w.to_ascii_lowercase()) {
Some(w) if w == "off" => AutoNumber::Off,
_ => {
let start = words.first().and_then(|w| w.parse::<f64>().ok());
let step = words.get(1).and_then(|w| w.parse::<f64>().ok());
AutoNumber::On {
start: start.filter(|v| v.is_finite()).unwrap_or(1.0),
step: step.filter(|v| v.is_finite()).unwrap_or(1.0),
}
}
};
self.diagram.events.push(Event::AutoNumber(cmd));
Ok(())
}
fn message(&mut self, stmt: &str, line: usize) -> Result<(), ParseError> {
let Some((start, end, signal)) = find_arrow(stmt) else {
return Ok(());
};
let mut from = stmt[..start].trim();
let mut rest = stmt[end..].trim_start();
if let Some(f) = from.strip_suffix("()") {
from = f.trim_end();
}
if let Some(r) = rest.strip_prefix("()") {
rest = r.trim_start();
}
let mut activate_to = false;
let mut deactivate_from = false;
if let Some(r) = rest.strip_prefix('+') {
activate_to = true;
rest = r.trim_start();
} else if let Some(r) = rest.strip_prefix('-') {
deactivate_from = true;
rest = r.trim_start();
}
if let Some(r) = rest.strip_prefix("()") {
rest = r.trim_start();
}
let (to, text) = split_once_colon(rest);
let (from, to) = (from.trim().to_string(), to.trim().to_string());
if from.is_empty() || to.is_empty() {
return Ok(());
}
self.messages += 1;
if self.messages > MAX_MESSAGES {
return Err(ParseError::TooManyMessages {
limit: MAX_MESSAGES,
});
}
self.diagram.ensure(&from, ParticipantKind::Participant);
self.diagram.ensure(&to, ParticipantKind::Participant);
self.last_created = None;
self.last_destroyed = None;
self.diagram.events.push(Event::Message(Message {
from: from.clone(),
to: to.clone(),
text: decode_label(strip_wrap(&text)),
signal,
line,
}));
if activate_to {
self.push_activate(&to);
}
if deactivate_from {
self.push_deactivate(&from, line)?;
}
Ok(())
}
fn finish(mut self) -> Result<SequenceDiagram, ParseError> {
if let Some(open) = self.open.first() {
return Err(ParseError::UnclosedBlock {
keyword: open.keyword,
line: open.line,
});
}
if self.in_acc_descr {
let text = std::mem::take(&mut self.acc_descr_buf);
self.diagram.acc_descr = non_empty(text.trim().to_string());
}
if self.diagram.participants.is_empty() {
return Err(ParseError::NoParticipants);
}
Ok(self.diagram)
}
}
fn non_empty(s: String) -> Option<String> {
if s.trim().is_empty() {
None
} else {
Some(s)
}
}
fn split_once_colon(s: &str) -> (String, String) {
match s.find(':') {
Some(i) => (s[..i].to_string(), strip_wrap(&s[i + 1..]).to_string()),
None => (s.to_string(), String::new()),
}
}
fn split_as(s: &str) -> (&str, Option<&str>) {
let chars: Vec<(usize, char)> = s.char_indices().collect();
for (n, (i, c)) in chars.iter().enumerate() {
if !c.is_whitespace() {
continue;
}
let rest = &s[*i..];
let trimmed = rest.trim_start();
if !starts_ci(trimmed, "as") {
continue;
}
let after = &trimmed[2..];
if !after.chars().next().is_some_and(char::is_whitespace) {
continue;
}
let _ = n;
return (s[..*i].trim_end(), Some(after.trim_start()));
}
(s.trim(), None)
}
fn split_config(s: &str) -> (&str, Option<&str>) {
let Some(at) = s.find("@{") else {
return (s, None);
};
let Some(close) = s[at..].find('}') else {
return (s, None);
};
(s[..at].trim_end(), Some(&s[at + 2..at + close]))
}
fn config_value(config: &str, key: &str) -> Option<String> {
for part in config.split(',') {
let (k, v) = part.split_once(':')?;
let k = k.trim().trim_matches(['"', '\'']).trim();
if !k.eq_ignore_ascii_case(key) {
continue;
}
let v = v.trim().trim_matches(['"', '\'']).trim();
if v.is_empty() {
return None;
}
return Some(v.to_string());
}
None
}
fn split_box_data(s: &str) -> (Option<String>, String) {
let t = s.trim();
if t.is_empty() {
return (None, String::new());
}
for f in ["rgba", "rgb", "hsla", "hsl"] {
if starts_ci(t, f) {
let after = &t[f.len()..];
if after.trim_start().starts_with('(') {
if let Some(close) = t.find(')') {
return (
Some(t[..=close].to_string()),
t[close + 1..].trim().to_string(),
);
}
}
}
}
let (first, rest) = head_word(t);
if is_color_name(&first) {
return (Some(first), rest.to_string());
}
(None, t.to_string())
}
fn is_color_name(word: &str) -> bool {
const NAMES: &[&str] = &[
"transparent",
"aqua",
"aquamarine",
"black",
"blue",
"brown",
"coral",
"cyan",
"gold",
"gray",
"green",
"grey",
"indigo",
"khaki",
"lavender",
"lime",
"magenta",
"maroon",
"navy",
"olive",
"orange",
"pink",
"purple",
"red",
"salmon",
"silver",
"tan",
"teal",
"violet",
"wheat",
"white",
"yellow",
];
NAMES.contains(&word)
}
fn after_colon_keyword(stmt: &str, keyword: &str) -> Option<String> {
if !starts_ci(stmt, keyword) {
return None;
}
let after = stmt[keyword.len()..].trim_start();
Some(after.strip_prefix(':')?.trim().to_string())
}
pub fn is_actor(p: &Participant) -> bool {
p.kind == ParticipantKind::Actor
}