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_items: None,
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_items: None,
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> {
425 let (input, kind) = alt((
426 value(ParticipantKind::Participant, tag_no_case("participant")),
427 value(ParticipantKind::Actor, tag_no_case("actor")),
428 ))
429 .parse(input)?;
430
431 let (input, _) = space1.parse(input)?;
432
433 let (input, name) = parse_name(input)?;
435
436 let (input, alias) = opt(preceded(
438 (space1, tag_no_case("as"), space1),
439 parse_identifier,
440 ))
441 .parse(input)?;
442
443 Ok((
444 input,
445 Item::ParticipantDecl {
446 name: name.to_string(),
447 alias: alias.map(|s| s.to_string()),
448 kind,
449 },
450 ))
451}
452
453fn parse_name(input: &str) -> IResult<&str, &str> {
455 alt((
456 delimited(char('"'), take_until("\""), char('"')),
458 parse_identifier,
460 ))
461 .parse(input)
462}
463
464fn parse_identifier(input: &str) -> IResult<&str, &str> {
466 take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
467}
468
469fn parse_message(input: &str) -> IResult<&str, Item> {
472 let (input, from) = parse_name(input)?;
473 let (input, arrow) = parse_arrow(input)?;
474 let (input, modifiers) = parse_arrow_modifiers(input)?;
475 let (input, to) = parse_name(input)?;
476 let (input, _) = opt(char(':')).parse(input)?;
477 let (input, _) = space0.parse(input)?;
478 let text = input.trim().to_string();
479
480 Ok((
481 "",
482 Item::Message {
483 from: from.to_string(),
484 to: to.to_string(),
485 text,
486 arrow,
487 activate: modifiers.0,
488 deactivate: modifiers.1,
489 create: modifiers.2,
490 },
491 ))
492}
493
494fn parse_arrow(input: &str) -> IResult<&str, Arrow> {
497 alt((
498 value(Arrow::RESPONSE, tag("<-->")),
500 value(Arrow::SYNC, tag("<->")),
502 value(Arrow::RESPONSE_OPEN, tag("-->>")),
504 value(Arrow::RESPONSE, tag("-->")),
506 value(Arrow::SYNC_OPEN, tag("->>")),
508 map(delimited(tag("->("), digit1, char(')')), |n: &str| Arrow {
510 line: LineStyle::Solid,
511 head: ArrowHead::Filled,
512 delay: n.parse().ok(),
513 }),
514 value(Arrow::SYNC, tag("->")),
516 ))
517 .parse(input)
518}
519
520fn parse_arrow_modifiers(input: &str) -> IResult<&str, (bool, bool, bool)> {
522 let (input, mods) = take_while(|c| c == '+' || c == '-' || c == '*').parse(input)?;
523 let activate = mods.contains('+');
524 let deactivate = mods.contains('-');
525 let create = mods.contains('*');
526 Ok((input, (activate, deactivate, create)))
527}
528
529fn parse_note(input: &str) -> IResult<&str, Item> {
531 let (input, _) = tag_no_case("note").parse(input)?;
532 let (input, _) = space1.parse(input)?;
533
534 let (input, position) = alt((
535 value(NotePosition::Left, pair(tag_no_case("left"), space1)),
536 value(NotePosition::Right, pair(tag_no_case("right"), space1)),
537 value(NotePosition::Over, tag_no_case("")),
538 ))
539 .parse(input)?;
540
541 let (input, position) = if position == NotePosition::Over {
542 let (input, _) = tag_no_case("over").parse(input)?;
543 (input, NotePosition::Over)
544 } else {
545 let (input, _) = tag_no_case("of").parse(input)?;
546 (input, position)
547 };
548
549 let (input, _) = space1.parse(input)?;
550
551 let (input, participants) =
553 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
554
555 let (input, _) = opt(char(':')).parse(input)?;
556 let (input, _) = space0.parse(input)?;
557 let text = input.trim().to_string();
558
559 Ok((
560 "",
561 Item::Note {
562 position,
563 participants: participants.into_iter().map(|s| s.to_string()).collect(),
564 text,
565 },
566 ))
567}
568
569fn parse_state(input: &str) -> IResult<&str, Item> {
571 let (input, _) = tag_no_case("state").parse(input)?;
572 let (input, _) = space1.parse(input)?;
573 let (input, _) = tag_no_case("over").parse(input)?;
574 let (input, _) = space1.parse(input)?;
575
576 let (input, participants) =
578 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
579
580 let (input, _) = opt(char(':')).parse(input)?;
581 let (input, _) = space0.parse(input)?;
582 let text = input.trim().to_string();
583
584 Ok((
585 "",
586 Item::State {
587 participants: participants.into_iter().map(|s| s.to_string()).collect(),
588 text,
589 },
590 ))
591}
592
593fn parse_ref_single_line(input: &str) -> IResult<&str, Item> {
595 let (input, _) = tag_no_case("ref").parse(input)?;
596 let (input, _) = space1.parse(input)?;
597 let (input, _) = tag_no_case("over").parse(input)?;
598 let (input, _) = space1.parse(input)?;
599
600 let (input, participants) =
602 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
603
604 let (input, _) = char(':').parse(input)?;
605 let (input, _) = space0.parse(input)?;
606 let text = input.trim().to_string();
607
608 Ok((
609 "",
610 Item::Ref {
611 participants: participants.into_iter().map(|s| s.to_string()).collect(),
612 text,
613 input_from: None,
614 input_label: None,
615 output_to: None,
616 output_label: None,
617 },
618 ))
619}
620
621fn parse_option(input: &str) -> IResult<&str, Item> {
623 let (input, _) = tag_no_case("option").parse(input)?;
624 let (input, _) = space1.parse(input)?;
625 let (input, key) = take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)?;
626 let (input, _) = char('=').parse(input)?;
627 let (_input, value) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
628
629 Ok((
630 "",
631 Item::DiagramOption {
632 key: key.to_string(),
633 value: value.to_string(),
634 },
635 ))
636}
637
638fn parse_activate(input: &str) -> IResult<&str, Item> {
640 let (input, _) = tag_no_case("activate").parse(input)?;
641 let (input, _) = space1.parse(input)?;
642 let (_input, participant) = parse_name(input)?;
643 Ok((
644 "",
645 Item::Activate {
646 participant: participant.to_string(),
647 },
648 ))
649}
650
651fn parse_deactivate(input: &str) -> IResult<&str, Item> {
653 let (input, _) = tag_no_case("deactivate").parse(input)?;
654 let (input, _) = space1.parse(input)?;
655 let (_input, participant) = parse_name(input)?;
656 Ok((
657 "",
658 Item::Deactivate {
659 participant: participant.to_string(),
660 },
661 ))
662}
663
664fn parse_destroy(input: &str) -> IResult<&str, Item> {
666 let (input, _) = tag_no_case("destroy").parse(input)?;
667 let (input, _) = space1.parse(input)?;
668 let (_input, participant) = parse_name(input)?;
669 Ok((
670 "",
671 Item::Destroy {
672 participant: participant.to_string(),
673 },
674 ))
675}
676
677fn parse_autonumber(input: &str) -> IResult<&str, Item> {
679 let (input, _) = tag_no_case("autonumber").parse(input)?;
680
681 let (_input, rest) =
682 opt(preceded(space1, take_while1(|c: char| !c.is_whitespace()))).parse(input)?;
683
684 let (enabled, start) = match rest {
685 Some("off") => (false, None),
686 Some(n) => (true, n.parse().ok()),
687 None => (true, None),
688 };
689
690 Ok(("", Item::Autonumber { enabled, start }))
691}
692
693fn parse_block_keyword(input: &str) -> IResult<&str, Item> {
695 alt((parse_block_start, parse_else, parse_end)).parse(input)
696}
697
698fn parse_block_start(input: &str) -> IResult<&str, Item> {
700 let (input, kind) = alt((
701 value(BlockKind::Alt, tag_no_case("alt")),
702 value(BlockKind::Opt, tag_no_case("opt")),
703 value(BlockKind::Loop, tag_no_case("loop")),
704 value(BlockKind::Par, tag_no_case("par")),
705 value(BlockKind::Seq, tag_no_case("seq")),
706 ))
707 .parse(input)?;
708
709 let (input, _) = space0.parse(input)?;
710 let label = input.trim().to_string();
711
712 Ok((
714 "",
715 Item::Block {
716 kind,
717 label,
718 items: vec![],
719 else_items: None,
720 },
721 ))
722}
723
724fn parse_else(input: &str) -> IResult<&str, Item> {
726 let (input, _) = tag_no_case("else").parse(input)?;
727 let (input, _) = space0.parse(input)?;
728 let label = input.trim().to_string();
729
730 Ok((
732 "",
733 Item::Block {
734 kind: BlockKind::Alt, label: format!("__ELSE__{}", label),
736 items: vec![],
737 else_items: None,
738 },
739 ))
740}
741
742fn parse_end(input: &str) -> IResult<&str, Item> {
744 let trimmed = input.trim().to_lowercase();
745 if trimmed.starts_with("end note") || trimmed.starts_with("end ref") {
747 return Err(nom::Err::Error(nom::error::Error::new(
748 input,
749 nom::error::ErrorKind::Tag,
750 )));
751 }
752 let (_input, _) = tag_no_case("end").parse(input)?;
753 Ok((
754 "",
755 Item::Block {
756 kind: BlockKind::Alt, label: "__END__".to_string(),
758 items: vec![],
759 else_items: None,
760 },
761 ))
762}
763
764fn build_blocks(items: Vec<Item>) -> Result<Vec<Item>, ParseError> {
766 let mut result = Vec::new();
767 let mut stack: Vec<(BlockKind, String, Vec<Item>, Option<Vec<Item>>, bool)> = Vec::new();
768
769 for item in items {
770 match &item {
771 Item::Block { label, .. } if label == "__END__" => {
772 if let Some((kind, label, items, else_items, _)) = stack.pop() {
774 let block = Item::Block {
775 kind,
776 label,
777 items,
778 else_items,
779 };
780 if let Some(parent) = stack.last_mut() {
781 if parent.4 {
782 parent.3.get_or_insert_with(Vec::new).push(block);
784 } else {
785 parent.2.push(block);
786 }
787 } else {
788 result.push(block);
789 }
790 }
791 }
792 Item::Block { label, .. } if label.starts_with("__ELSE__") => {
793 if let Some(parent) = stack.last_mut() {
795 parent.4 = true; parent.3 = Some(Vec::new());
797 }
798 }
799 Item::Block {
800 kind,
801 label,
802 items,
803 else_items,
804 } if !label.starts_with("__") => {
805 if matches!(kind, BlockKind::Parallel | BlockKind::Serial) || !items.is_empty() {
807 let block = Item::Block {
809 kind: *kind,
810 label: label.clone(),
811 items: items.clone(),
812 else_items: else_items.clone(),
813 };
814 if let Some(parent) = stack.last_mut() {
815 if parent.4 {
816 parent.3.get_or_insert_with(Vec::new).push(block);
817 } else {
818 parent.2.push(block);
819 }
820 } else {
821 result.push(block);
822 }
823 } else {
824 stack.push((*kind, label.clone(), Vec::new(), None, false));
826 }
827 }
828 _ => {
829 if let Some(parent) = stack.last_mut() {
831 if parent.4 {
832 parent.3.get_or_insert_with(Vec::new).push(item);
834 } else {
835 parent.2.push(item);
836 }
837 } else {
838 result.push(item);
839 }
840 }
841 }
842 }
843
844 Ok(result)
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850
851 #[test]
852 fn test_simple_message() {
853 let result = parse("Alice->Bob: Hello").unwrap();
854 assert_eq!(result.items.len(), 1);
855 match &result.items[0] {
856 Item::Message { from, to, text, .. } => {
857 assert_eq!(from, "Alice");
858 assert_eq!(to, "Bob");
859 assert_eq!(text, "Hello");
860 }
861 _ => panic!("Expected Message"),
862 }
863 }
864
865 #[test]
866 fn test_participant_decl() {
867 let result = parse("participant Alice\nactor Bob").unwrap();
868 assert_eq!(result.items.len(), 2);
869 }
870
871 #[test]
872 fn test_note() {
873 let result = parse("note over Alice: Hello").unwrap();
874 assert_eq!(result.items.len(), 1);
875 match &result.items[0] {
876 Item::Note {
877 position,
878 participants,
879 text,
880 } => {
881 assert_eq!(*position, NotePosition::Over);
882 assert_eq!(participants, &["Alice"]);
883 assert_eq!(text, "Hello");
884 }
885 _ => panic!("Expected Note"),
886 }
887 }
888
889 #[test]
890 fn test_opt_block() {
891 let result = parse("opt condition\nAlice->Bob: Hello\nend").unwrap();
892 assert_eq!(result.items.len(), 1);
893 match &result.items[0] {
894 Item::Block {
895 kind, label, items, ..
896 } => {
897 assert_eq!(*kind, BlockKind::Opt);
898 assert_eq!(label, "condition");
899 assert_eq!(items.len(), 1);
900 }
901 _ => panic!("Expected Block"),
902 }
903 }
904
905 #[test]
906 fn test_alt_else_block() {
907 let result =
908 parse("alt success\nAlice->Bob: OK\nelse failure\nAlice->Bob: Error\nend").unwrap();
909 assert_eq!(result.items.len(), 1);
910 match &result.items[0] {
911 Item::Block {
912 kind,
913 label,
914 items,
915 else_items,
916 ..
917 } => {
918 assert_eq!(*kind, BlockKind::Alt);
919 assert_eq!(label, "success");
920 assert_eq!(items.len(), 1);
921 assert!(else_items.is_some());
922 assert_eq!(else_items.as_ref().unwrap().len(), 1);
923 }
924 _ => panic!("Expected Block"),
925 }
926 }
927
928 #[test]
930 fn test_comment() {
931 let result = parse("# This is a comment\nAlice->Bob: Hello").unwrap();
932 assert_eq!(result.items.len(), 1);
933 match &result.items[0] {
934 Item::Message { from, to, text, .. } => {
935 assert_eq!(from, "Alice");
936 assert_eq!(to, "Bob");
937 assert_eq!(text, "Hello");
938 }
939 _ => panic!("Expected Message"),
940 }
941 }
942
943 #[test]
945 fn test_multiline_note() {
946 let input = r#"note left of Alice
947Line 1
948Line 2
949end note"#;
950 let result = parse(input).unwrap();
951 assert_eq!(result.items.len(), 1);
952 match &result.items[0] {
953 Item::Note {
954 position,
955 participants,
956 text,
957 } => {
958 assert_eq!(*position, NotePosition::Left);
959 assert_eq!(participants, &["Alice"]);
960 assert_eq!(text, "Line 1\\nLine 2");
961 }
962 _ => panic!("Expected Note"),
963 }
964 }
965
966 #[test]
968 fn test_state() {
969 let result = parse("state over Server: LISTEN").unwrap();
970 assert_eq!(result.items.len(), 1);
971 match &result.items[0] {
972 Item::State { participants, text } => {
973 assert_eq!(participants, &["Server"]);
974 assert_eq!(text, "LISTEN");
975 }
976 _ => panic!("Expected State"),
977 }
978 }
979
980 #[test]
982 fn test_ref() {
983 let result = parse("ref over Alice, Bob: See other diagram").unwrap();
984 assert_eq!(result.items.len(), 1);
985 match &result.items[0] {
986 Item::Ref {
987 participants, text, ..
988 } => {
989 assert_eq!(participants, &["Alice", "Bob"]);
990 assert_eq!(text, "See other diagram");
991 }
992 _ => panic!("Expected Ref"),
993 }
994 }
995
996 #[test]
997 fn test_ref_input_signal_multiline() {
998 let input = r#"Alice->ref over Bob, Carol: Input signal
999line 1
1000line 2
1001end ref-->Alice: Output signal"#;
1002 let result = parse(input).unwrap();
1003 assert_eq!(result.items.len(), 1);
1004 match &result.items[0] {
1005 Item::Ref {
1006 participants,
1007 text,
1008 input_from,
1009 input_label,
1010 output_to,
1011 output_label,
1012 } => {
1013 assert_eq!(participants, &["Bob", "Carol"]);
1014 assert_eq!(text, "line 1\\nline 2");
1015 assert_eq!(input_from.as_deref(), Some("Alice"));
1016 assert_eq!(input_label.as_deref(), Some("Input signal"));
1017 assert_eq!(output_to.as_deref(), Some("Alice"));
1018 assert_eq!(output_label.as_deref(), Some("Output signal"));
1019 }
1020 _ => panic!("Expected Ref"),
1021 }
1022 }
1023
1024 #[test]
1026 fn test_option() {
1027 let result = parse("option footer=none").unwrap();
1028 assert_eq!(result.items.len(), 1);
1029 match &result.items[0] {
1030 Item::DiagramOption { key, value } => {
1031 assert_eq!(key, "footer");
1032 assert_eq!(value, "none");
1033 }
1034 _ => panic!("Expected DiagramOption"),
1035 }
1036 }
1037
1038 #[test]
1040 fn test_quoted_name_with_colon() {
1041 let result = parse(r#"":Alice"->":Bob": Hello"#).unwrap();
1042 assert_eq!(result.items.len(), 1);
1043 match &result.items[0] {
1044 Item::Message { from, to, text, .. } => {
1045 assert_eq!(from, ":Alice");
1046 assert_eq!(to, ":Bob");
1047 assert_eq!(text, "Hello");
1048 }
1049 _ => panic!("Expected Message"),
1050 }
1051 }
1052}