1use nom::{
4 branch::alt,
5 bytes::complete::{tag, tag_no_case, take_until, take_while, take_while1},
6 character::complete::{char, digit1, space0, space1},
7 combinator::{map, opt, value},
8 multi::separated_list1,
9 sequence::{delimited, pair, preceded},
10 IResult, Parser,
11};
12
13use crate::ast::*;
14
15#[derive(Debug, Clone, thiserror::Error)]
17pub enum ParseError {
18 #[error("Parse error at line {line}: {message}")]
19 SyntaxError { line: usize, message: String },
20}
21
22pub fn parse(input: &str) -> Result<Diagram, ParseError> {
24 let mut items = Vec::new();
25 let mut title = None;
26 let lines: Vec<&str> = input.lines().collect();
27 let mut i = 0;
28
29 while i < lines.len() {
30 let line = lines[i];
31 let trimmed = line.trim();
32
33 if trimmed.is_empty() {
35 i += 1;
36 continue;
37 }
38
39 if trimmed.starts_with('#') {
41 i += 1;
42 continue;
43 }
44
45 if line.starts_with(' ') && !trimmed.is_empty() && !line.starts_with(" ") {
47 items.push(Item::Description {
49 text: trimmed.to_string(),
50 });
51 i += 1;
52 continue;
53 }
54
55 if let Ok((_, t)) = parse_title(trimmed) {
57 title = Some(t);
58 i += 1;
59 continue;
60 }
61
62 if let Some((position, participants)) = parse_multiline_note_start(trimmed) {
64 let mut note_lines = Vec::new();
65 i += 1;
66 while i < lines.len() {
67 let note_line = lines[i].trim();
68 if note_line.eq_ignore_ascii_case("end note") {
69 break;
70 }
71 note_lines.push(note_line);
72 i += 1;
73 }
74 let text = note_lines.join("\\n");
75 items.push(Item::Note {
76 position,
77 participants,
78 text,
79 });
80 i += 1;
81 continue;
82 }
83
84 if let Some(ref_start) = parse_multiline_ref_start(trimmed) {
87 let mut ref_lines = Vec::new();
88 let mut output_to: Option<String> = None;
89 let mut output_label: Option<String> = None;
90 i += 1;
91 while i < lines.len() {
92 let ref_line = lines[i].trim();
93 if let Some((out_to, out_label)) = parse_ref_end(ref_line) {
95 output_to = out_to;
96 output_label = out_label;
97 break;
98 }
99 ref_lines.push(ref_line);
100 i += 1;
101 }
102 let text = ref_lines.join("\\n");
103 items.push(Item::Ref {
104 participants: ref_start.participants,
105 text,
106 input_from: ref_start.input_from,
107 input_label: ref_start.input_label,
108 output_to,
109 output_label,
110 });
111 i += 1;
112 continue;
113 }
114
115 if let Some((kind, remaining)) = parse_brace_block_start(trimmed) {
117 let mut block_items = Vec::new();
118 let mut brace_depth = 1;
119
120 let after_brace = remaining.trim();
122 if !after_brace.is_empty() && after_brace != "{" {
123 }
125
126 i += 1;
127 while i < lines.len() && brace_depth > 0 {
128 let block_line = lines[i].trim();
129
130 if block_line == "}" {
131 brace_depth -= 1;
132 if brace_depth == 0 {
133 break;
134 }
135 i += 1;
136 continue;
137 }
138
139 if !block_line.is_empty() && !block_line.starts_with('#') {
140 if let Some((nested_kind, _)) = parse_brace_block_start(block_line) {
142 let mut nested_items = Vec::new();
144 let mut nested_depth = 1;
145 i += 1;
146
147 while i < lines.len() && nested_depth > 0 {
148 let nested_line = lines[i].trim();
149 if nested_line == "}" {
150 nested_depth -= 1;
151 if nested_depth == 0 {
152 break;
153 }
154 } else if nested_line.ends_with('{') {
155 nested_depth += 1;
156 }
157
158 if nested_depth > 0
159 && !nested_line.is_empty()
160 && !nested_line.starts_with('#')
161 {
162 if let Ok((_, item)) = parse_line(nested_line) {
163 nested_items.push(item);
164 }
165 }
166 i += 1;
167 }
168
169 block_items.push(Item::Block {
170 kind: nested_kind,
171 label: String::new(),
172 items: nested_items,
173 else_sections: vec![],
174 });
175 } else if let Ok((_, item)) = parse_line(block_line) {
176 block_items.push(item);
177 }
178 }
179 i += 1;
180 }
181
182 items.push(Item::Block {
183 kind,
184 label: String::new(),
185 items: block_items,
186 else_sections: vec![],
187 });
188 i += 1;
189 continue;
190 }
191
192 match parse_line(trimmed) {
194 Ok((_, item)) => {
195 items.push(item);
196 }
197 Err(e) => {
198 return Err(ParseError::SyntaxError {
199 line: i + 1,
200 message: format!("Failed to parse: {:?}", e),
201 });
202 }
203 }
204 i += 1;
205 }
206
207 let items = build_blocks(items)?;
209
210 let mut options = DiagramOptions::default();
212 for item in &items {
213 if let Item::DiagramOption { key, value } = item {
214 if key.eq_ignore_ascii_case("footer") {
215 options.footer = match value.to_lowercase().as_str() {
216 "none" => FooterStyle::None,
217 "bar" => FooterStyle::Bar,
218 "box" => FooterStyle::Box,
219 _ => FooterStyle::Box,
220 };
221 }
222 }
223 }
224
225 Ok(Diagram {
226 title,
227 items,
228 options,
229 })
230}
231
232fn parse_multiline_note_start(input: &str) -> Option<(NotePosition, Vec<String>)> {
234 let input_lower = input.to_lowercase();
235
236 if !input_lower.starts_with("note ") || input.contains(':') {
238 return None;
239 }
240
241 let rest = &input[5..].trim();
242
243 let (position, after_pos) = if rest.to_lowercase().starts_with("left of ") {
245 (NotePosition::Left, &rest[8..])
246 } else if rest.to_lowercase().starts_with("right of ") {
247 (NotePosition::Right, &rest[9..])
248 } else if rest.to_lowercase().starts_with("over ") {
249 (NotePosition::Over, &rest[5..])
250 } else {
251 return None;
252 };
253
254 let participants: Vec<String> = after_pos
256 .split(',')
257 .map(|s| s.trim().to_string())
258 .filter(|s| !s.is_empty())
259 .collect();
260
261 if participants.is_empty() {
262 return None;
263 }
264
265 Some((position, participants))
266}
267
268struct RefStartResult {
270 participants: Vec<String>,
271 input_from: Option<String>,
272 input_label: Option<String>,
273}
274
275fn parse_multiline_ref_start(input: &str) -> Option<RefStartResult> {
278 let mut input_from: Option<String> = None;
279 let mut input_label: Option<String> = None;
280 let mut rest_str = input.to_string();
281
282 if let Some(arrow_pos) = input.to_lowercase().find("->") {
284 let after_arrow = input[arrow_pos + 2..].trim_start();
285 if after_arrow.to_lowercase().starts_with("ref over") {
286 input_from = Some(input[..arrow_pos].trim().to_string());
287 rest_str = after_arrow.to_string(); }
289 }
290
291 let rest_lower = rest_str.to_lowercase();
292
293 if !rest_lower.starts_with("ref over ") && !rest_lower.starts_with("ref over") {
295 return None;
296 }
297
298 let after_ref_over = if rest_lower.starts_with("ref over ") {
300 &rest_str[9..]
301 } else {
302 &rest_str[8..]
303 };
304 let after_ref_over = after_ref_over.trim();
305
306 let (participants_str, label) = if let Some(colon_pos) = after_ref_over.find(':') {
308 let parts = after_ref_over.split_at(colon_pos);
309 (parts.0.trim(), Some(parts.1[1..].trim()))
310 } else {
311 (after_ref_over, None)
312 };
313
314 let participants: Vec<String> = participants_str
316 .split(',')
317 .map(|s| s.trim().to_string())
318 .filter(|s| !s.is_empty())
319 .collect();
320
321 if participants.is_empty() {
322 return None;
323 }
324
325 if input_from.is_some() && label.is_some() {
327 input_label = label.map(|s| s.to_string());
328 }
329
330 if label.is_some() && input_from.is_none() {
333 return None;
335 }
336
337 Some(RefStartResult {
338 participants,
339 input_from,
340 input_label,
341 })
342}
343
344fn parse_ref_end(line: &str) -> Option<(Option<String>, Option<String>)> {
347 let trimmed = line.trim();
348 let lower = trimmed.to_lowercase();
349
350 if !lower.starts_with("end ref") {
351 return None;
352 }
353
354 let rest = &trimmed[7..]; if let Some(arrow_pos) = rest.find("-->") {
358 let after_arrow = &rest[arrow_pos + 3..];
359 if let Some(colon_pos) = after_arrow.find(':') {
361 let to = after_arrow[..colon_pos].trim().to_string();
362 let label = after_arrow[colon_pos + 1..].trim().to_string();
363 return Some((Some(to), Some(label)));
364 } else {
365 let to = after_arrow.trim().to_string();
366 return Some((Some(to), None));
367 }
368 }
369
370 Some((None, None))
372}
373
374fn parse_brace_block_start(input: &str) -> Option<(BlockKind, &str)> {
376 let trimmed = input.trim();
377
378 if let Some(rest) = trimmed.strip_prefix("parallel") {
380 let rest = rest.trim();
381 if rest.starts_with('{') {
382 return Some((BlockKind::Parallel, &rest[1..]));
383 }
384 }
385
386 if let Some(rest) = trimmed.strip_prefix("serial") {
388 let rest = rest.trim();
389 if rest.starts_with('{') {
390 return Some((BlockKind::Serial, &rest[1..]));
391 }
392 }
393
394 None
395}
396
397fn parse_line(input: &str) -> IResult<&str, Item> {
399 alt((
400 parse_state,
401 parse_ref_single_line,
402 parse_option,
403 parse_participant_decl,
404 parse_note,
405 parse_activate,
406 parse_deactivate,
407 parse_destroy,
408 parse_autonumber,
409 parse_block_keyword,
410 parse_message,
411 ))
412 .parse(input)
413}
414
415fn parse_title(input: &str) -> IResult<&str, String> {
417 let (input, _) = tag_no_case("title").parse(input)?;
418 let (input, _) = space1.parse(input)?;
419 let title = input.trim().to_string();
420 Ok(("", title))
421}
422
423fn parse_participant_decl(input: &str) -> IResult<&str, Item> {
426 let (input, kind) = alt((
427 value(ParticipantKind::Participant, tag_no_case("participant")),
428 value(ParticipantKind::Actor, tag_no_case("actor")),
429 ))
430 .parse(input)?;
431
432 let (input, _) = space1.parse(input)?;
433
434 let (remaining, name, alias) = if input.starts_with('"') {
436 let (input, name) = delimited(char('"'), take_until("\""), char('"')).parse(input)?;
438 let (input, alias) = opt(preceded(
440 (space1, tag_no_case("as"), space1),
441 parse_identifier,
442 ))
443 .parse(input)?;
444 (input, name, alias)
445 } else {
446 let lower_input = input.to_lowercase();
449 if let Some(as_pos) = lower_input.find(" as ") {
450 let name = input[..as_pos].trim();
451 let after_as = &input[as_pos + 4..]; let (_, alias) = parse_identifier(after_as.trim_start())?;
453 ("", name, Some(alias))
454 } else {
455 let name = input.trim();
457 ("", name, None)
458 }
459 };
460
461 Ok((
462 remaining,
463 Item::ParticipantDecl {
464 name: name.to_string(),
465 alias: alias.map(|s| s.to_string()),
466 kind,
467 },
468 ))
469}
470
471fn parse_name(input: &str) -> IResult<&str, &str> {
473 alt((
474 tag("["),
476 tag("]"),
477 delimited(char('"'), take_until("\""), char('"')),
479 parse_identifier,
481 ))
482 .parse(input)
483}
484
485fn parse_identifier(input: &str) -> IResult<&str, &str> {
487 take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
488}
489
490fn parse_message(input: &str) -> IResult<&str, Item> {
494 let arrow_patterns = [
496 ("<-->", Arrow::RESPONSE),
497 ("-->>", Arrow::RESPONSE_OPEN),
498 ("<->", Arrow::SYNC),
499 ("-->", Arrow::RESPONSE),
500 ("->>", Arrow::SYNC_OPEN),
501 ("->", Arrow::SYNC),
502 ];
503
504 let mut arrow_pos: Option<(usize, usize, Arrow)> = None; if let Some(delay_start) = input.find("->(") {
509 if let Some(paren_end) = input[delay_start + 3..].find(')') {
510 let delay_str = &input[delay_start + 3..delay_start + 3 + paren_end];
511 if let Ok(delay_val) = delay_str.parse::<u32>() {
512 arrow_pos = Some((delay_start, delay_start + 4 + paren_end, Arrow {
513 line: LineStyle::Solid,
514 head: ArrowHead::Filled,
515 delay: Some(delay_val),
516 }));
517 }
518 }
519 }
520
521 if arrow_pos.is_none() {
523 for (pattern, arrow) in &arrow_patterns {
524 if let Some(pos) = input.find(pattern) {
525 arrow_pos = Some((pos, pos + pattern.len(), *arrow));
526 break;
527 }
528 }
529 }
530
531 let (arrow_start, arrow_end, arrow) = arrow_pos.ok_or_else(|| {
532 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
533 })?;
534
535 let from = input[..arrow_start].trim();
537 let after_arrow = &input[arrow_end..];
538
539 let (after_mods, modifiers) = parse_arrow_modifiers(after_arrow)?;
541
542 let (to, text) = if after_mods.trim_start().starts_with('"') {
544 let trimmed = after_mods.trim_start();
546 if let Some(end_quote) = trimmed[1..].find('"') {
547 let to_name = &trimmed[1..end_quote + 1];
548 let rest = &trimmed[end_quote + 2..];
549 let text = rest.trim_start_matches(':').trim().to_string();
550 (to_name, text)
551 } else {
552 return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)));
553 }
554 } else {
555 if let Some(colon_pos) = after_mods.find(':') {
557 let to_name = after_mods[..colon_pos].trim();
558 let text = after_mods[colon_pos + 1..].trim().to_string();
559 (to_name, text)
560 } else {
561 (after_mods.trim(), String::new())
563 }
564 };
565
566 let from = if from.starts_with('"') && from.ends_with('"') && from.len() > 2 {
568 &from[1..from.len() - 1]
569 } else {
570 from
571 };
572
573 Ok((
574 "",
575 Item::Message {
576 from: from.to_string(),
577 to: to.to_string(),
578 text,
579 arrow,
580 activate: modifiers.0,
581 deactivate: modifiers.1,
582 create: modifiers.2,
583 },
584 ))
585}
586
587fn parse_arrow(input: &str) -> IResult<&str, Arrow> {
590 alt((
591 value(Arrow::RESPONSE, tag("<-->")),
593 value(Arrow::SYNC, tag("<->")),
595 value(Arrow::RESPONSE_OPEN, tag("-->>")),
597 value(Arrow::RESPONSE, tag("-->")),
599 value(Arrow::SYNC_OPEN, tag("->>")),
601 map(delimited(tag("->("), digit1, char(')')), |n: &str| Arrow {
603 line: LineStyle::Solid,
604 head: ArrowHead::Filled,
605 delay: n.parse().ok(),
606 }),
607 value(Arrow::SYNC, tag("->")),
609 ))
610 .parse(input)
611}
612
613fn parse_arrow_modifiers(input: &str) -> IResult<&str, (bool, bool, bool)> {
615 let (input, mods) = take_while(|c| c == '+' || c == '-' || c == '*').parse(input)?;
616 let activate = mods.contains('+');
617 let deactivate = mods.contains('-');
618 let create = mods.contains('*');
619 Ok((input, (activate, deactivate, create)))
620}
621
622fn parse_note(input: &str) -> IResult<&str, Item> {
624 let (input, _) = tag_no_case("note").parse(input)?;
625 let (input, _) = space1.parse(input)?;
626
627 let (input, position) = alt((
628 value(NotePosition::Left, pair(tag_no_case("left"), space1)),
629 value(NotePosition::Right, pair(tag_no_case("right"), space1)),
630 value(NotePosition::Over, tag_no_case("")),
631 ))
632 .parse(input)?;
633
634 let (input, position) = if position == NotePosition::Over {
635 let (input, _) = tag_no_case("over").parse(input)?;
636 (input, NotePosition::Over)
637 } else {
638 let (input, _) = tag_no_case("of").parse(input)?;
639 (input, position)
640 };
641
642 let (input, _) = space1.parse(input)?;
643
644 let (input, participants) =
646 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
647
648 let (input, _) = opt(char(':')).parse(input)?;
649 let (input, _) = space0.parse(input)?;
650 let text = input.trim().to_string();
651
652 Ok((
653 "",
654 Item::Note {
655 position,
656 participants: participants.into_iter().map(|s| s.to_string()).collect(),
657 text,
658 },
659 ))
660}
661
662fn parse_state(input: &str) -> IResult<&str, Item> {
664 let (input, _) = tag_no_case("state").parse(input)?;
665 let (input, _) = space1.parse(input)?;
666 let (input, _) = tag_no_case("over").parse(input)?;
667 let (input, _) = space1.parse(input)?;
668
669 let (input, participants) =
671 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
672
673 let (input, _) = opt(char(':')).parse(input)?;
674 let (input, _) = space0.parse(input)?;
675 let text = input.trim().to_string();
676
677 Ok((
678 "",
679 Item::State {
680 participants: participants.into_iter().map(|s| s.to_string()).collect(),
681 text,
682 },
683 ))
684}
685
686fn parse_ref_single_line(input: &str) -> IResult<&str, Item> {
688 let (input, _) = tag_no_case("ref").parse(input)?;
689 let (input, _) = space1.parse(input)?;
690 let (input, _) = tag_no_case("over").parse(input)?;
691 let (input, _) = space1.parse(input)?;
692
693 let (input, participants) =
695 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
696
697 let (input, _) = char(':').parse(input)?;
698 let (input, _) = space0.parse(input)?;
699 let text = input.trim().to_string();
700
701 Ok((
702 "",
703 Item::Ref {
704 participants: participants.into_iter().map(|s| s.to_string()).collect(),
705 text,
706 input_from: None,
707 input_label: None,
708 output_to: None,
709 output_label: None,
710 },
711 ))
712}
713
714fn parse_option(input: &str) -> IResult<&str, Item> {
716 let (input, _) = tag_no_case("option").parse(input)?;
717 let (input, _) = space1.parse(input)?;
718 let (input, key) = take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)?;
719 let (input, _) = char('=').parse(input)?;
720 let (_input, value) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
721
722 Ok((
723 "",
724 Item::DiagramOption {
725 key: key.to_string(),
726 value: value.to_string(),
727 },
728 ))
729}
730
731fn parse_activate(input: &str) -> IResult<&str, Item> {
733 let (input, _) = tag_no_case("activate").parse(input)?;
734 let (input, _) = space1.parse(input)?;
735 let (_input, participant) = parse_name(input)?;
736 Ok((
737 "",
738 Item::Activate {
739 participant: participant.to_string(),
740 },
741 ))
742}
743
744fn parse_deactivate(input: &str) -> IResult<&str, Item> {
746 let (input, _) = tag_no_case("deactivate").parse(input)?;
747 let (input, _) = space1.parse(input)?;
748 let (_input, participant) = parse_name(input)?;
749 Ok((
750 "",
751 Item::Deactivate {
752 participant: participant.to_string(),
753 },
754 ))
755}
756
757fn parse_destroy(input: &str) -> IResult<&str, Item> {
759 let (input, _) = tag_no_case("destroy").parse(input)?;
760 let (input, _) = space1.parse(input)?;
761 let (_input, participant) = parse_name(input)?;
762 Ok((
763 "",
764 Item::Destroy {
765 participant: participant.to_string(),
766 },
767 ))
768}
769
770fn parse_autonumber(input: &str) -> IResult<&str, Item> {
772 let (input, _) = tag_no_case("autonumber").parse(input)?;
773
774 let (_input, rest) =
775 opt(preceded(space1, take_while1(|c: char| !c.is_whitespace()))).parse(input)?;
776
777 let (enabled, start) = match rest {
778 Some("off") => (false, None),
779 Some(n) => (true, n.parse().ok()),
780 None => (true, None),
781 };
782
783 Ok(("", Item::Autonumber { enabled, start }))
784}
785
786fn parse_block_keyword(input: &str) -> IResult<&str, Item> {
788 alt((parse_block_start, parse_else, parse_end)).parse(input)
789}
790
791fn parse_block_start(input: &str) -> IResult<&str, Item> {
793 let (input, kind) = alt((
794 value(BlockKind::Alt, tag_no_case("alt")),
795 value(BlockKind::Opt, tag_no_case("opt")),
796 value(BlockKind::Loop, tag_no_case("loop")),
797 value(BlockKind::Par, tag_no_case("par")),
798 value(BlockKind::Seq, tag_no_case("seq")),
799 ))
800 .parse(input)?;
801
802 let (input, _) = space0.parse(input)?;
803 let label = input.trim().to_string();
804
805 Ok((
807 "",
808 Item::Block {
809 kind,
810 label,
811 items: vec![],
812 else_sections: vec![],
813 },
814 ))
815}
816
817fn parse_else(input: &str) -> IResult<&str, Item> {
819 let (input, _) = tag_no_case("else").parse(input)?;
820 let (input, _) = space0.parse(input)?;
821 let label = input.trim().to_string();
822
823 Ok((
825 "",
826 Item::Block {
827 kind: BlockKind::Alt, label: format!("__ELSE__{}", label),
829 items: vec![],
830 else_sections: vec![],
831 },
832 ))
833}
834
835fn parse_end(input: &str) -> IResult<&str, Item> {
837 let trimmed = input.trim().to_lowercase();
838 if trimmed.starts_with("end note") || trimmed.starts_with("end ref") {
840 return Err(nom::Err::Error(nom::error::Error::new(
841 input,
842 nom::error::ErrorKind::Tag,
843 )));
844 }
845 let (_input, _) = tag_no_case("end").parse(input)?;
846 Ok((
847 "",
848 Item::Block {
849 kind: BlockKind::Alt, label: "__END__".to_string(),
851 items: vec![],
852 else_sections: vec![],
853 },
854 ))
855}
856
857fn build_blocks(items: Vec<Item>) -> Result<Vec<Item>, ParseError> {
859 use crate::ast::ElseSection;
860
861 let mut result = Vec::new();
862 struct StackEntry {
864 kind: BlockKind,
865 label: String,
866 items: Vec<Item>,
867 else_sections: Vec<ElseSection>,
868 current_else_items: Vec<Item>,
869 current_else_label: Option<String>,
870 in_else_branch: bool,
871 }
872 let mut stack: Vec<StackEntry> = Vec::new();
873
874 for item in items {
875 match &item {
876 Item::Block { label, .. } if label == "__END__" => {
877 if let Some(mut entry) = stack.pop() {
879 if entry.in_else_branch && !entry.current_else_items.is_empty() {
881 entry.else_sections.push(ElseSection {
882 label: entry.current_else_label.take(),
883 items: std::mem::take(&mut entry.current_else_items),
884 });
885 }
886 let block = Item::Block {
887 kind: entry.kind,
888 label: entry.label,
889 items: entry.items,
890 else_sections: entry.else_sections,
891 };
892 if let Some(parent) = stack.last_mut() {
893 if parent.in_else_branch {
894 parent.current_else_items.push(block);
895 } else {
896 parent.items.push(block);
897 }
898 } else {
899 result.push(block);
900 }
901 }
902 }
903 Item::Block { label, .. } if label.starts_with("__ELSE__") => {
904 let else_label_text = label.strip_prefix("__ELSE__").unwrap_or("").to_string();
906 if let Some(entry) = stack.last_mut() {
907 if entry.in_else_branch && !entry.current_else_items.is_empty() {
909 entry.else_sections.push(ElseSection {
910 label: entry.current_else_label.take(),
911 items: std::mem::take(&mut entry.current_else_items),
912 });
913 }
914 entry.in_else_branch = true;
916 entry.current_else_items = Vec::new();
917 entry.current_else_label = if else_label_text.is_empty() {
918 None
919 } else {
920 Some(else_label_text)
921 };
922 }
923 }
924 Item::Block {
925 kind,
926 label,
927 items,
928 else_sections,
929 ..
930 } if !label.starts_with("__") => {
931 if matches!(kind, BlockKind::Parallel | BlockKind::Serial) || !items.is_empty() {
933 let block = Item::Block {
935 kind: *kind,
936 label: label.clone(),
937 items: items.clone(),
938 else_sections: else_sections.clone(),
939 };
940 if let Some(parent) = stack.last_mut() {
941 if parent.in_else_branch {
942 parent.current_else_items.push(block);
943 } else {
944 parent.items.push(block);
945 }
946 } else {
947 result.push(block);
948 }
949 } else {
950 stack.push(StackEntry {
952 kind: *kind,
953 label: label.clone(),
954 items: Vec::new(),
955 else_sections: Vec::new(),
956 current_else_items: Vec::new(),
957 current_else_label: None,
958 in_else_branch: false,
959 });
960 }
961 }
962 _ => {
963 if let Some(parent) = stack.last_mut() {
965 if parent.in_else_branch {
966 parent.current_else_items.push(item);
967 } else {
968 parent.items.push(item);
969 }
970 } else {
971 result.push(item);
972 }
973 }
974 }
975 }
976
977 Ok(result)
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983
984 #[test]
985 fn test_simple_message() {
986 let result = parse("Alice->Bob: Hello").unwrap();
987 assert_eq!(result.items.len(), 1);
988 match &result.items[0] {
989 Item::Message { from, to, text, .. } => {
990 assert_eq!(from, "Alice");
991 assert_eq!(to, "Bob");
992 assert_eq!(text, "Hello");
993 }
994 _ => panic!("Expected Message"),
995 }
996 }
997
998 #[test]
999 fn test_participant_decl() {
1000 let result = parse("participant Alice\nactor Bob").unwrap();
1001 assert_eq!(result.items.len(), 2);
1002 }
1003
1004 #[test]
1005 fn test_note() {
1006 let result = parse("note over Alice: Hello").unwrap();
1007 assert_eq!(result.items.len(), 1);
1008 match &result.items[0] {
1009 Item::Note {
1010 position,
1011 participants,
1012 text,
1013 } => {
1014 assert_eq!(*position, NotePosition::Over);
1015 assert_eq!(participants, &["Alice"]);
1016 assert_eq!(text, "Hello");
1017 }
1018 _ => panic!("Expected Note"),
1019 }
1020 }
1021
1022 #[test]
1023 fn test_opt_block() {
1024 let result = parse("opt condition\nAlice->Bob: Hello\nend").unwrap();
1025 assert_eq!(result.items.len(), 1);
1026 match &result.items[0] {
1027 Item::Block {
1028 kind, label, items, ..
1029 } => {
1030 assert_eq!(*kind, BlockKind::Opt);
1031 assert_eq!(label, "condition");
1032 assert_eq!(items.len(), 1);
1033 }
1034 _ => panic!("Expected Block"),
1035 }
1036 }
1037
1038 #[test]
1039 fn test_alt_else_block() {
1040 let result =
1041 parse("alt success\nAlice->Bob: OK\nelse failure\nAlice->Bob: Error\nend").unwrap();
1042 assert_eq!(result.items.len(), 1);
1043 match &result.items[0] {
1044 Item::Block {
1045 kind,
1046 label,
1047 items,
1048 else_sections,
1049 ..
1050 } => {
1051 assert_eq!(*kind, BlockKind::Alt);
1052 assert_eq!(label, "success");
1053 assert_eq!(items.len(), 1);
1054 assert_eq!(else_sections.len(), 1);
1055 assert_eq!(else_sections[0].items.len(), 1);
1056 }
1057 _ => panic!("Expected Block"),
1058 }
1059 }
1060
1061 #[test]
1063 fn test_comment() {
1064 let result = parse("# This is a comment\nAlice->Bob: Hello").unwrap();
1065 assert_eq!(result.items.len(), 1);
1066 match &result.items[0] {
1067 Item::Message { from, to, text, .. } => {
1068 assert_eq!(from, "Alice");
1069 assert_eq!(to, "Bob");
1070 assert_eq!(text, "Hello");
1071 }
1072 _ => panic!("Expected Message"),
1073 }
1074 }
1075
1076 #[test]
1078 fn test_multiline_note() {
1079 let input = r#"note left of Alice
1080Line 1
1081Line 2
1082end note"#;
1083 let result = parse(input).unwrap();
1084 assert_eq!(result.items.len(), 1);
1085 match &result.items[0] {
1086 Item::Note {
1087 position,
1088 participants,
1089 text,
1090 } => {
1091 assert_eq!(*position, NotePosition::Left);
1092 assert_eq!(participants, &["Alice"]);
1093 assert_eq!(text, "Line 1\\nLine 2");
1094 }
1095 _ => panic!("Expected Note"),
1096 }
1097 }
1098
1099 #[test]
1101 fn test_state() {
1102 let result = parse("state over Server: LISTEN").unwrap();
1103 assert_eq!(result.items.len(), 1);
1104 match &result.items[0] {
1105 Item::State { participants, text } => {
1106 assert_eq!(participants, &["Server"]);
1107 assert_eq!(text, "LISTEN");
1108 }
1109 _ => panic!("Expected State"),
1110 }
1111 }
1112
1113 #[test]
1115 fn test_ref() {
1116 let result = parse("ref over Alice, Bob: See other diagram").unwrap();
1117 assert_eq!(result.items.len(), 1);
1118 match &result.items[0] {
1119 Item::Ref {
1120 participants, text, ..
1121 } => {
1122 assert_eq!(participants, &["Alice", "Bob"]);
1123 assert_eq!(text, "See other diagram");
1124 }
1125 _ => panic!("Expected Ref"),
1126 }
1127 }
1128
1129 #[test]
1130 fn test_ref_input_signal_multiline() {
1131 let input = r#"Alice->ref over Bob, Carol: Input signal
1132line 1
1133line 2
1134end ref-->Alice: Output signal"#;
1135 let result = parse(input).unwrap();
1136 assert_eq!(result.items.len(), 1);
1137 match &result.items[0] {
1138 Item::Ref {
1139 participants,
1140 text,
1141 input_from,
1142 input_label,
1143 output_to,
1144 output_label,
1145 } => {
1146 assert_eq!(participants, &["Bob", "Carol"]);
1147 assert_eq!(text, "line 1\\nline 2");
1148 assert_eq!(input_from.as_deref(), Some("Alice"));
1149 assert_eq!(input_label.as_deref(), Some("Input signal"));
1150 assert_eq!(output_to.as_deref(), Some("Alice"));
1151 assert_eq!(output_label.as_deref(), Some("Output signal"));
1152 }
1153 _ => panic!("Expected Ref"),
1154 }
1155 }
1156
1157 #[test]
1159 fn test_option() {
1160 let result = parse("option footer=none").unwrap();
1161 assert_eq!(result.items.len(), 1);
1162 match &result.items[0] {
1163 Item::DiagramOption { key, value } => {
1164 assert_eq!(key, "footer");
1165 assert_eq!(value, "none");
1166 }
1167 _ => panic!("Expected DiagramOption"),
1168 }
1169 }
1170
1171 #[test]
1173 fn test_quoted_name_with_colon() {
1174 let result = parse(r#"":Alice"->":Bob": Hello"#).unwrap();
1175 assert_eq!(result.items.len(), 1);
1176 match &result.items[0] {
1177 Item::Message { from, to, text, .. } => {
1178 assert_eq!(from, ":Alice");
1179 assert_eq!(to, ":Bob");
1180 assert_eq!(text, "Hello");
1181 }
1182 _ => panic!("Expected Message"),
1183 }
1184 }
1185
1186 #[test]
1188 fn test_participant_name_with_spaces() {
1189 let input = r#"participant OSD Frontend
1190participant OSD Backend
1191OSD Frontend->OSD Backend: API Request
1192OSD Backend-->OSD Frontend: Response"#;
1193 let result = parse(input).unwrap();
1194 assert_eq!(result.items.len(), 4);
1195
1196 match &result.items[0] {
1198 Item::ParticipantDecl { name, alias, kind } => {
1199 assert_eq!(name, "OSD Frontend");
1200 assert_eq!(*alias, None);
1201 assert_eq!(*kind, ParticipantKind::Participant);
1202 }
1203 _ => panic!("Expected ParticipantDecl"),
1204 }
1205 match &result.items[1] {
1206 Item::ParticipantDecl { name, alias, kind } => {
1207 assert_eq!(name, "OSD Backend");
1208 assert_eq!(*alias, None);
1209 assert_eq!(*kind, ParticipantKind::Participant);
1210 }
1211 _ => panic!("Expected ParticipantDecl"),
1212 }
1213
1214 match &result.items[2] {
1216 Item::Message { from, to, text, .. } => {
1217 assert_eq!(from, "OSD Frontend");
1218 assert_eq!(to, "OSD Backend");
1219 assert_eq!(text, "API Request");
1220 }
1221 _ => panic!("Expected Message"),
1222 }
1223 match &result.items[3] {
1224 Item::Message { from, to, text, .. } => {
1225 assert_eq!(from, "OSD Backend");
1226 assert_eq!(to, "OSD Frontend");
1227 assert_eq!(text, "Response");
1228 }
1229 _ => panic!("Expected Message"),
1230 }
1231 }
1232
1233 #[test]
1235 fn test_participant_with_spaces_and_alias() {
1236 let input = "participant My Service Name as MSN";
1237 let result = parse(input).unwrap();
1238 assert_eq!(result.items.len(), 1);
1239 match &result.items[0] {
1240 Item::ParticipantDecl { name, alias, .. } => {
1241 assert_eq!(name, "My Service Name");
1242 assert_eq!(alias.as_deref(), Some("MSN"));
1243 }
1244 _ => panic!("Expected ParticipantDecl"),
1245 }
1246 }
1247}