#[derive(Debug, Clone, PartialEq)]
pub enum LineType {
Solid, SolidArrow, SolidCross, Dotted, DottedArrow, DottedCross, Point, }
#[derive(Debug, Clone, PartialEq)]
pub enum NotePlacement {
RightOf,
LeftOf,
Over,
}
#[derive(Debug, Clone)]
pub enum ParticipantKind {
Participant,
Actor,
}
#[derive(Debug, Clone)]
pub struct Participant {
pub name: String,
pub alias: String, pub kind: ParticipantKind,
}
#[derive(Debug, Clone)]
pub struct Message {
pub from: String,
pub to: String,
pub text: String,
pub line_type: LineType,
pub activate: i32,
}
#[derive(Debug, Clone)]
pub struct NoteItem {
pub actors: Vec<String>, pub placement: NotePlacement,
pub text: String,
}
#[derive(Debug, Clone)]
pub enum SeqItem {
Participant(Participant),
Message(Message),
Note(NoteItem),
LoopStart(String),
AltStart(String),
AltElse(String),
OptStart(String),
ParStart(String),
ParAnd(String),
CriticalStart(String),
CriticalOption(String),
BreakStart(String),
RectStart(String),
BoxStart {
label: String,
#[allow(dead_code)]
color: Option<String>,
},
BlockEnd,
Activate(String),
Deactivate(String),
AutoNumber {
#[allow(dead_code)]
start: Option<u32>,
#[allow(dead_code)]
step: Option<u32>,
#[allow(dead_code)]
enabled: bool,
},
Link {
#[allow(dead_code)]
actor: String,
#[allow(dead_code)]
title: String,
#[allow(dead_code)]
url: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct SequenceDiagram {
pub items: Vec<SeqItem>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<SequenceDiagram> {
let mut diag = SequenceDiagram::default();
let mut in_yaml = false;
for line in input.lines() {
let trimmed = line.trim();
if trimmed == "---" {
in_yaml = !in_yaml;
continue;
}
if in_yaml {
continue;
}
if trimmed.is_empty()
|| trimmed.starts_with("sequenceDiagram")
|| trimmed.starts_with("%%")
|| trimmed.starts_with("accTitle")
|| trimmed.starts_with("accDescr")
{
continue;
}
if let Some(item) = parse_line(trimmed) {
diag.items.push(item);
}
}
crate::error::ParseResult::ok(diag)
}
fn parse_line(s: &str) -> Option<SeqItem> {
if let Some(rest) = s.strip_prefix("participant ") {
return Some(parse_participant(rest, ParticipantKind::Participant));
}
if let Some(rest) = s.strip_prefix("actor ") {
return Some(parse_participant(rest, ParticipantKind::Actor));
}
if let Some(rest) = s.strip_prefix("activate ") {
return Some(SeqItem::Activate(rest.trim().to_string()));
}
if let Some(rest) = s.strip_prefix("deactivate ") {
return Some(SeqItem::Deactivate(rest.trim().to_string()));
}
if s == "autonumber" {
return Some(SeqItem::AutoNumber {
start: None,
step: None,
enabled: true,
});
}
if s == "autonumber off" {
return Some(SeqItem::AutoNumber {
start: None,
step: None,
enabled: false,
});
}
if let Some(rest) = s.strip_prefix("autonumber ") {
let rest = rest.trim();
if rest == "off" {
return Some(SeqItem::AutoNumber {
start: None,
step: None,
enabled: false,
});
}
let parts: Vec<&str> = rest.split_whitespace().collect();
let start = parts.first().and_then(|s| s.parse().ok());
let step = parts.get(1).and_then(|s| s.parse().ok());
return Some(SeqItem::AutoNumber {
start,
step,
enabled: true,
});
}
if s.starts_with("Note ") || s.starts_with("note ") {
return parse_note(s);
}
if let Some(rest) = s.strip_prefix("loop ") {
return Some(SeqItem::LoopStart(rest.trim().to_string()));
}
if s == "loop" {
return Some(SeqItem::LoopStart(String::new()));
}
if s == "end" {
return Some(SeqItem::BlockEnd);
}
if let Some(rest) = s.strip_prefix("alt ") {
return Some(SeqItem::AltStart(rest.trim().to_string()));
}
if s == "alt" {
return Some(SeqItem::AltStart(String::new()));
}
if let Some(rest) = s.strip_prefix("else ") {
return Some(SeqItem::AltElse(rest.trim().to_string()));
}
if s == "else" {
return Some(SeqItem::AltElse(String::new()));
}
if let Some(rest) = s.strip_prefix("opt ") {
return Some(SeqItem::OptStart(rest.trim().to_string()));
}
if s == "opt" {
return Some(SeqItem::OptStart(String::new()));
}
if let Some(rest) = s.strip_prefix("par ") {
return Some(SeqItem::ParStart(rest.trim().to_string()));
}
if s == "par" {
return Some(SeqItem::ParStart(String::new()));
}
if let Some(rest) = s.strip_prefix("and ") {
return Some(SeqItem::ParAnd(rest.trim().to_string()));
}
if s == "and" {
return Some(SeqItem::ParAnd(String::new()));
}
if let Some(rest) = s.strip_prefix("critical ") {
return Some(SeqItem::CriticalStart(rest.trim().to_string()));
}
if s == "critical" {
return Some(SeqItem::CriticalStart(String::new()));
}
if let Some(rest) = s.strip_prefix("option ") {
return Some(SeqItem::CriticalOption(rest.trim().to_string()));
}
if s == "option" {
return Some(SeqItem::CriticalOption(String::new()));
}
if let Some(rest) = s.strip_prefix("break ") {
return Some(SeqItem::BreakStart(rest.trim().to_string()));
}
if s == "break" {
return Some(SeqItem::BreakStart(String::new()));
}
if let Some(rest) = s.strip_prefix("rect ") {
return Some(SeqItem::RectStart(rest.trim().to_string()));
}
if let Some(rest) = s.strip_prefix("box ") {
return Some(parse_box(rest.trim()));
}
if s == "box" {
return Some(SeqItem::BoxStart {
label: String::new(),
color: None,
});
}
if let Some(rest) = s.strip_prefix("link ") {
if let Some(colon) = rest.find(':') {
let actor = rest[..colon].trim().to_string();
let after = rest[colon + 1..].trim();
if let Some(at) = after.find('@') {
let title = after[..at].trim().to_string();
let url = after[at + 1..].trim().to_string();
return Some(SeqItem::Link { actor, title, url });
}
}
return None;
}
if s.starts_with("links ") {
return None;
}
parse_message(s)
}
fn parse_participant(s: &str, kind: ParticipantKind) -> SeqItem {
if let Some(idx) = s.find(" as ") {
let name = s[..idx].trim().to_string();
let alias = s[idx + 4..].trim().to_string();
return SeqItem::Participant(Participant { name, alias, kind });
}
let name = s.trim().to_string();
SeqItem::Participant(Participant {
alias: name.clone(),
name,
kind,
})
}
fn parse_box(rest: &str) -> SeqItem {
let rest = rest.trim();
let (color, label) = if rest.starts_with("rgba(")
|| rest.starts_with("rgb(")
|| rest.starts_with('#')
|| rest.starts_with("hsl(")
{
if let Some(space) = rest.find(' ') {
(
Some(rest[..space].to_string()),
rest[space + 1..].trim().to_string(),
)
} else {
(Some(rest.to_string()), String::new())
}
} else {
(None, rest.to_string())
};
SeqItem::BoxStart { label, color }
}
fn parse_note(s: &str) -> Option<SeqItem> {
let lower = s.to_lowercase();
let rest = if lower.starts_with("note ") {
&s[5..]
} else {
return None;
};
let (placement, rest) = if let Some(r) = rest.strip_prefix("right of ") {
(NotePlacement::RightOf, r)
} else if let Some(r) = rest.strip_prefix("left of ") {
(NotePlacement::LeftOf, r)
} else if let Some(r) = rest.strip_prefix("over ") {
(NotePlacement::Over, r)
} else {
return None;
};
let (actors_part, text) = if let Some(colon) = rest.find(':') {
(&rest[..colon], rest[colon + 1..].trim())
} else {
(rest, "")
};
let actors: Vec<String> = actors_part
.split(',')
.map(|a| a.trim().to_string())
.filter(|a| !a.is_empty())
.collect();
Some(SeqItem::Note(NoteItem {
actors,
placement,
text: text.to_string(),
}))
}
fn parse_message(s: &str) -> Option<SeqItem> {
let arrows: &[(&str, LineType)] = &[
("-->>x", LineType::DottedArrow),
("->>x", LineType::SolidArrow),
("-->>", LineType::DottedArrow),
("->>", LineType::SolidArrow),
("-->x", LineType::DottedCross),
("--x", LineType::DottedCross), ("->x", LineType::SolidCross),
("-x", LineType::SolidCross), ("-->", LineType::Dotted),
("->", LineType::Solid),
("--)", LineType::DottedArrow),
("-)", LineType::Point),
];
for (arrow, lt) in arrows {
if let Some(arrow_pos) = find_arrow(s, arrow) {
let from = s[..arrow_pos].trim().to_string();
let after_arrow = s[arrow_pos + arrow.len()..].trim();
let (target_part, msg_text) = if let Some(colon) = after_arrow.find(':') {
(&after_arrow[..colon], after_arrow[colon + 1..].trim())
} else {
(after_arrow, "")
};
let target_trimmed = target_part.trim();
let (to, activate) = if let Some(stripped) = target_trimmed.strip_prefix('+') {
(stripped.trim().to_string(), 1i32)
} else if let Some(stripped) = target_trimmed.strip_prefix('-') {
(stripped.trim().to_string(), -1i32)
} else {
(target_trimmed.to_string(), 0i32)
};
if from.is_empty() || to.is_empty() {
continue;
}
return Some(SeqItem::Message(Message {
from,
to,
text: msg_text.to_string(),
line_type: lt.clone(),
activate,
}));
}
}
None
}
fn find_arrow(s: &str, arrow: &str) -> Option<usize> {
let _bytes = s.as_bytes();
let alen = arrow.len();
for i in 0..s.len().saturating_sub(alen - 1) {
if &s[i..i + alen] == arrow {
return Some(i);
}
}
None
}