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 else_label: None,
175 });
176 } else if let Ok((_, item)) = parse_line(block_line) {
177 block_items.push(item);
178 }
179 }
180 i += 1;
181 }
182
183 items.push(Item::Block {
184 kind,
185 label: String::new(),
186 items: block_items,
187 else_items: None,
188 else_label: None,
189 });
190 i += 1;
191 continue;
192 }
193
194 match parse_line(trimmed) {
196 Ok((_, item)) => {
197 items.push(item);
198 }
199 Err(e) => {
200 return Err(ParseError::SyntaxError {
201 line: i + 1,
202 message: format!("Failed to parse: {:?}", e),
203 });
204 }
205 }
206 i += 1;
207 }
208
209 let items = build_blocks(items)?;
211
212 let mut options = DiagramOptions::default();
214 for item in &items {
215 if let Item::DiagramOption { key, value } = item {
216 if key.eq_ignore_ascii_case("footer") {
217 options.footer = match value.to_lowercase().as_str() {
218 "none" => FooterStyle::None,
219 "bar" => FooterStyle::Bar,
220 "box" => FooterStyle::Box,
221 _ => FooterStyle::Box,
222 };
223 }
224 }
225 }
226
227 Ok(Diagram {
228 title,
229 items,
230 options,
231 })
232}
233
234fn parse_multiline_note_start(input: &str) -> Option<(NotePosition, Vec<String>)> {
236 let input_lower = input.to_lowercase();
237
238 if !input_lower.starts_with("note ") || input.contains(':') {
240 return None;
241 }
242
243 let rest = &input[5..].trim();
244
245 let (position, after_pos) = if rest.to_lowercase().starts_with("left of ") {
247 (NotePosition::Left, &rest[8..])
248 } else if rest.to_lowercase().starts_with("right of ") {
249 (NotePosition::Right, &rest[9..])
250 } else if rest.to_lowercase().starts_with("over ") {
251 (NotePosition::Over, &rest[5..])
252 } else {
253 return None;
254 };
255
256 let participants: Vec<String> = after_pos
258 .split(',')
259 .map(|s| s.trim().to_string())
260 .filter(|s| !s.is_empty())
261 .collect();
262
263 if participants.is_empty() {
264 return None;
265 }
266
267 Some((position, participants))
268}
269
270struct RefStartResult {
272 participants: Vec<String>,
273 input_from: Option<String>,
274 input_label: Option<String>,
275}
276
277fn parse_multiline_ref_start(input: &str) -> Option<RefStartResult> {
280 let mut input_from: Option<String> = None;
281 let mut input_label: Option<String> = None;
282 let mut rest_str = input.to_string();
283
284 if let Some(arrow_pos) = input.to_lowercase().find("->") {
286 let after_arrow = input[arrow_pos + 2..].trim_start();
287 if after_arrow.to_lowercase().starts_with("ref over") {
288 input_from = Some(input[..arrow_pos].trim().to_string());
289 rest_str = after_arrow.to_string(); }
291 }
292
293 let rest_lower = rest_str.to_lowercase();
294
295 if !rest_lower.starts_with("ref over ") && !rest_lower.starts_with("ref over") {
297 return None;
298 }
299
300 let after_ref_over = if rest_lower.starts_with("ref over ") {
302 &rest_str[9..]
303 } else {
304 &rest_str[8..]
305 };
306 let after_ref_over = after_ref_over.trim();
307
308 let (participants_str, label) = if let Some(colon_pos) = after_ref_over.find(':') {
310 let parts = after_ref_over.split_at(colon_pos);
311 (parts.0.trim(), Some(parts.1[1..].trim()))
312 } else {
313 (after_ref_over, None)
314 };
315
316 let participants: Vec<String> = participants_str
318 .split(',')
319 .map(|s| s.trim().to_string())
320 .filter(|s| !s.is_empty())
321 .collect();
322
323 if participants.is_empty() {
324 return None;
325 }
326
327 if input_from.is_some() && label.is_some() {
329 input_label = label.map(|s| s.to_string());
330 }
331
332 if label.is_some() && input_from.is_none() {
335 return None;
337 }
338
339 Some(RefStartResult {
340 participants,
341 input_from,
342 input_label,
343 })
344}
345
346fn parse_ref_end(line: &str) -> Option<(Option<String>, Option<String>)> {
349 let trimmed = line.trim();
350 let lower = trimmed.to_lowercase();
351
352 if !lower.starts_with("end ref") {
353 return None;
354 }
355
356 let rest = &trimmed[7..]; if let Some(arrow_pos) = rest.find("-->") {
360 let after_arrow = &rest[arrow_pos + 3..];
361 if let Some(colon_pos) = after_arrow.find(':') {
363 let to = after_arrow[..colon_pos].trim().to_string();
364 let label = after_arrow[colon_pos + 1..].trim().to_string();
365 return Some((Some(to), Some(label)));
366 } else {
367 let to = after_arrow.trim().to_string();
368 return Some((Some(to), None));
369 }
370 }
371
372 Some((None, None))
374}
375
376fn parse_brace_block_start(input: &str) -> Option<(BlockKind, &str)> {
378 let trimmed = input.trim();
379
380 if let Some(rest) = trimmed.strip_prefix("parallel") {
382 let rest = rest.trim();
383 if rest.starts_with('{') {
384 return Some((BlockKind::Parallel, &rest[1..]));
385 }
386 }
387
388 if let Some(rest) = trimmed.strip_prefix("serial") {
390 let rest = rest.trim();
391 if rest.starts_with('{') {
392 return Some((BlockKind::Serial, &rest[1..]));
393 }
394 }
395
396 None
397}
398
399fn parse_line(input: &str) -> IResult<&str, Item> {
401 alt((
402 parse_state,
403 parse_ref_single_line,
404 parse_option,
405 parse_participant_decl,
406 parse_note,
407 parse_activate,
408 parse_deactivate,
409 parse_destroy,
410 parse_autonumber,
411 parse_block_keyword,
412 parse_message,
413 ))
414 .parse(input)
415}
416
417fn parse_title(input: &str) -> IResult<&str, String> {
419 let (input, _) = tag_no_case("title").parse(input)?;
420 let (input, _) = space1.parse(input)?;
421 let title = input.trim().to_string();
422 Ok(("", title))
423}
424
425fn parse_participant_decl(input: &str) -> IResult<&str, Item> {
427 let (input, kind) = alt((
428 value(ParticipantKind::Participant, tag_no_case("participant")),
429 value(ParticipantKind::Actor, tag_no_case("actor")),
430 ))
431 .parse(input)?;
432
433 let (input, _) = space1.parse(input)?;
434
435 let (input, name) = parse_name(input)?;
437
438 let (input, alias) = opt(preceded(
440 (space1, tag_no_case("as"), space1),
441 parse_identifier,
442 ))
443 .parse(input)?;
444
445 Ok((
446 input,
447 Item::ParticipantDecl {
448 name: name.to_string(),
449 alias: alias.map(|s| s.to_string()),
450 kind,
451 },
452 ))
453}
454
455fn parse_name(input: &str) -> IResult<&str, &str> {
457 alt((
458 delimited(char('"'), take_until("\""), char('"')),
460 parse_identifier,
462 ))
463 .parse(input)
464}
465
466fn parse_identifier(input: &str) -> IResult<&str, &str> {
468 take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
469}
470
471fn parse_message(input: &str) -> IResult<&str, Item> {
474 let (input, from) = parse_name(input)?;
475 let (input, arrow) = parse_arrow(input)?;
476 let (input, modifiers) = parse_arrow_modifiers(input)?;
477 let (input, to) = parse_name(input)?;
478 let (input, _) = opt(char(':')).parse(input)?;
479 let (input, _) = space0.parse(input)?;
480 let text = input.trim().to_string();
481
482 Ok((
483 "",
484 Item::Message {
485 from: from.to_string(),
486 to: to.to_string(),
487 text,
488 arrow,
489 activate: modifiers.0,
490 deactivate: modifiers.1,
491 create: modifiers.2,
492 },
493 ))
494}
495
496fn parse_arrow(input: &str) -> IResult<&str, Arrow> {
499 alt((
500 value(Arrow::RESPONSE, tag("<-->")),
502 value(Arrow::SYNC, tag("<->")),
504 value(Arrow::RESPONSE_OPEN, tag("-->>")),
506 value(Arrow::RESPONSE, tag("-->")),
508 value(Arrow::SYNC_OPEN, tag("->>")),
510 map(delimited(tag("->("), digit1, char(')')), |n: &str| Arrow {
512 line: LineStyle::Solid,
513 head: ArrowHead::Filled,
514 delay: n.parse().ok(),
515 }),
516 value(Arrow::SYNC, tag("->")),
518 ))
519 .parse(input)
520}
521
522fn parse_arrow_modifiers(input: &str) -> IResult<&str, (bool, bool, bool)> {
524 let (input, mods) = take_while(|c| c == '+' || c == '-' || c == '*').parse(input)?;
525 let activate = mods.contains('+');
526 let deactivate = mods.contains('-');
527 let create = mods.contains('*');
528 Ok((input, (activate, deactivate, create)))
529}
530
531fn parse_note(input: &str) -> IResult<&str, Item> {
533 let (input, _) = tag_no_case("note").parse(input)?;
534 let (input, _) = space1.parse(input)?;
535
536 let (input, position) = alt((
537 value(NotePosition::Left, pair(tag_no_case("left"), space1)),
538 value(NotePosition::Right, pair(tag_no_case("right"), space1)),
539 value(NotePosition::Over, tag_no_case("")),
540 ))
541 .parse(input)?;
542
543 let (input, position) = if position == NotePosition::Over {
544 let (input, _) = tag_no_case("over").parse(input)?;
545 (input, NotePosition::Over)
546 } else {
547 let (input, _) = tag_no_case("of").parse(input)?;
548 (input, position)
549 };
550
551 let (input, _) = space1.parse(input)?;
552
553 let (input, participants) =
555 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
556
557 let (input, _) = opt(char(':')).parse(input)?;
558 let (input, _) = space0.parse(input)?;
559 let text = input.trim().to_string();
560
561 Ok((
562 "",
563 Item::Note {
564 position,
565 participants: participants.into_iter().map(|s| s.to_string()).collect(),
566 text,
567 },
568 ))
569}
570
571fn parse_state(input: &str) -> IResult<&str, Item> {
573 let (input, _) = tag_no_case("state").parse(input)?;
574 let (input, _) = space1.parse(input)?;
575 let (input, _) = tag_no_case("over").parse(input)?;
576 let (input, _) = space1.parse(input)?;
577
578 let (input, participants) =
580 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
581
582 let (input, _) = opt(char(':')).parse(input)?;
583 let (input, _) = space0.parse(input)?;
584 let text = input.trim().to_string();
585
586 Ok((
587 "",
588 Item::State {
589 participants: participants.into_iter().map(|s| s.to_string()).collect(),
590 text,
591 },
592 ))
593}
594
595fn parse_ref_single_line(input: &str) -> IResult<&str, Item> {
597 let (input, _) = tag_no_case("ref").parse(input)?;
598 let (input, _) = space1.parse(input)?;
599 let (input, _) = tag_no_case("over").parse(input)?;
600 let (input, _) = space1.parse(input)?;
601
602 let (input, participants) =
604 separated_list1((space0, char(','), space0), parse_name).parse(input)?;
605
606 let (input, _) = char(':').parse(input)?;
607 let (input, _) = space0.parse(input)?;
608 let text = input.trim().to_string();
609
610 Ok((
611 "",
612 Item::Ref {
613 participants: participants.into_iter().map(|s| s.to_string()).collect(),
614 text,
615 input_from: None,
616 input_label: None,
617 output_to: None,
618 output_label: None,
619 },
620 ))
621}
622
623fn parse_option(input: &str) -> IResult<&str, Item> {
625 let (input, _) = tag_no_case("option").parse(input)?;
626 let (input, _) = space1.parse(input)?;
627 let (input, key) = take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)?;
628 let (input, _) = char('=').parse(input)?;
629 let (_input, value) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
630
631 Ok((
632 "",
633 Item::DiagramOption {
634 key: key.to_string(),
635 value: value.to_string(),
636 },
637 ))
638}
639
640fn parse_activate(input: &str) -> IResult<&str, Item> {
642 let (input, _) = tag_no_case("activate").parse(input)?;
643 let (input, _) = space1.parse(input)?;
644 let (_input, participant) = parse_name(input)?;
645 Ok((
646 "",
647 Item::Activate {
648 participant: participant.to_string(),
649 },
650 ))
651}
652
653fn parse_deactivate(input: &str) -> IResult<&str, Item> {
655 let (input, _) = tag_no_case("deactivate").parse(input)?;
656 let (input, _) = space1.parse(input)?;
657 let (_input, participant) = parse_name(input)?;
658 Ok((
659 "",
660 Item::Deactivate {
661 participant: participant.to_string(),
662 },
663 ))
664}
665
666fn parse_destroy(input: &str) -> IResult<&str, Item> {
668 let (input, _) = tag_no_case("destroy").parse(input)?;
669 let (input, _) = space1.parse(input)?;
670 let (_input, participant) = parse_name(input)?;
671 Ok((
672 "",
673 Item::Destroy {
674 participant: participant.to_string(),
675 },
676 ))
677}
678
679fn parse_autonumber(input: &str) -> IResult<&str, Item> {
681 let (input, _) = tag_no_case("autonumber").parse(input)?;
682
683 let (_input, rest) =
684 opt(preceded(space1, take_while1(|c: char| !c.is_whitespace()))).parse(input)?;
685
686 let (enabled, start) = match rest {
687 Some("off") => (false, None),
688 Some(n) => (true, n.parse().ok()),
689 None => (true, None),
690 };
691
692 Ok(("", Item::Autonumber { enabled, start }))
693}
694
695fn parse_block_keyword(input: &str) -> IResult<&str, Item> {
697 alt((parse_block_start, parse_else, parse_end)).parse(input)
698}
699
700fn parse_block_start(input: &str) -> IResult<&str, Item> {
702 let (input, kind) = alt((
703 value(BlockKind::Alt, tag_no_case("alt")),
704 value(BlockKind::Opt, tag_no_case("opt")),
705 value(BlockKind::Loop, tag_no_case("loop")),
706 value(BlockKind::Par, tag_no_case("par")),
707 value(BlockKind::Seq, tag_no_case("seq")),
708 ))
709 .parse(input)?;
710
711 let (input, _) = space0.parse(input)?;
712 let label = input.trim().to_string();
713
714 Ok((
716 "",
717 Item::Block {
718 kind,
719 label,
720 items: vec![],
721 else_items: None,
722 else_label: None,
723 },
724 ))
725}
726
727fn parse_else(input: &str) -> IResult<&str, Item> {
729 let (input, _) = tag_no_case("else").parse(input)?;
730 let (input, _) = space0.parse(input)?;
731 let label = input.trim().to_string();
732
733 Ok((
735 "",
736 Item::Block {
737 kind: BlockKind::Alt, label: format!("__ELSE__{}", label),
739 items: vec![],
740 else_items: None,
741 else_label: None,
742 },
743 ))
744}
745
746fn parse_end(input: &str) -> IResult<&str, Item> {
748 let trimmed = input.trim().to_lowercase();
749 if trimmed.starts_with("end note") || trimmed.starts_with("end ref") {
751 return Err(nom::Err::Error(nom::error::Error::new(
752 input,
753 nom::error::ErrorKind::Tag,
754 )));
755 }
756 let (_input, _) = tag_no_case("end").parse(input)?;
757 Ok((
758 "",
759 Item::Block {
760 kind: BlockKind::Alt, label: "__END__".to_string(),
762 items: vec![],
763 else_items: None,
764 else_label: None,
765 },
766 ))
767}
768
769fn build_blocks(items: Vec<Item>) -> Result<Vec<Item>, ParseError> {
771 let mut result = Vec::new();
772 let mut stack: Vec<(BlockKind, String, Vec<Item>, Option<Vec<Item>>, bool, Option<String>)> = Vec::new();
774
775 for item in items {
776 match &item {
777 Item::Block { label, .. } if label == "__END__" => {
778 if let Some((kind, label, items, else_items, _, else_label)) = stack.pop() {
780 let block = Item::Block {
781 kind,
782 label,
783 items,
784 else_items,
785 else_label,
786 };
787 if let Some(parent) = stack.last_mut() {
788 if parent.4 {
789 parent.3.get_or_insert_with(Vec::new).push(block);
791 } else {
792 parent.2.push(block);
793 }
794 } else {
795 result.push(block);
796 }
797 }
798 }
799 Item::Block { label, .. } if label.starts_with("__ELSE__") => {
800 let else_label_text = label.strip_prefix("__ELSE__").unwrap_or("").to_string();
802 if let Some(parent) = stack.last_mut() {
803 parent.4 = true; parent.3 = Some(Vec::new());
805 parent.5 = if else_label_text.is_empty() {
806 None
807 } else {
808 Some(else_label_text)
809 };
810 }
811 }
812 Item::Block {
813 kind,
814 label,
815 items,
816 else_items,
817 ..
818 } if !label.starts_with("__") => {
819 if matches!(kind, BlockKind::Parallel | BlockKind::Serial) || !items.is_empty() {
821 let block = Item::Block {
823 kind: *kind,
824 label: label.clone(),
825 items: items.clone(),
826 else_items: else_items.clone(),
827 else_label: None,
828 };
829 if let Some(parent) = stack.last_mut() {
830 if parent.4 {
831 parent.3.get_or_insert_with(Vec::new).push(block);
832 } else {
833 parent.2.push(block);
834 }
835 } else {
836 result.push(block);
837 }
838 } else {
839 stack.push((*kind, label.clone(), Vec::new(), None, false, None));
841 }
842 }
843 _ => {
844 if let Some(parent) = stack.last_mut() {
846 if parent.4 {
847 parent.3.get_or_insert_with(Vec::new).push(item);
849 } else {
850 parent.2.push(item);
851 }
852 } else {
853 result.push(item);
854 }
855 }
856 }
857 }
858
859 Ok(result)
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn test_simple_message() {
868 let result = parse("Alice->Bob: Hello").unwrap();
869 assert_eq!(result.items.len(), 1);
870 match &result.items[0] {
871 Item::Message { from, to, text, .. } => {
872 assert_eq!(from, "Alice");
873 assert_eq!(to, "Bob");
874 assert_eq!(text, "Hello");
875 }
876 _ => panic!("Expected Message"),
877 }
878 }
879
880 #[test]
881 fn test_participant_decl() {
882 let result = parse("participant Alice\nactor Bob").unwrap();
883 assert_eq!(result.items.len(), 2);
884 }
885
886 #[test]
887 fn test_note() {
888 let result = parse("note over Alice: Hello").unwrap();
889 assert_eq!(result.items.len(), 1);
890 match &result.items[0] {
891 Item::Note {
892 position,
893 participants,
894 text,
895 } => {
896 assert_eq!(*position, NotePosition::Over);
897 assert_eq!(participants, &["Alice"]);
898 assert_eq!(text, "Hello");
899 }
900 _ => panic!("Expected Note"),
901 }
902 }
903
904 #[test]
905 fn test_opt_block() {
906 let result = parse("opt condition\nAlice->Bob: Hello\nend").unwrap();
907 assert_eq!(result.items.len(), 1);
908 match &result.items[0] {
909 Item::Block {
910 kind, label, items, ..
911 } => {
912 assert_eq!(*kind, BlockKind::Opt);
913 assert_eq!(label, "condition");
914 assert_eq!(items.len(), 1);
915 }
916 _ => panic!("Expected Block"),
917 }
918 }
919
920 #[test]
921 fn test_alt_else_block() {
922 let result =
923 parse("alt success\nAlice->Bob: OK\nelse failure\nAlice->Bob: Error\nend").unwrap();
924 assert_eq!(result.items.len(), 1);
925 match &result.items[0] {
926 Item::Block {
927 kind,
928 label,
929 items,
930 else_items,
931 ..
932 } => {
933 assert_eq!(*kind, BlockKind::Alt);
934 assert_eq!(label, "success");
935 assert_eq!(items.len(), 1);
936 assert!(else_items.is_some());
937 assert_eq!(else_items.as_ref().unwrap().len(), 1);
938 }
939 _ => panic!("Expected Block"),
940 }
941 }
942
943 #[test]
945 fn test_comment() {
946 let result = parse("# This is a comment\nAlice->Bob: Hello").unwrap();
947 assert_eq!(result.items.len(), 1);
948 match &result.items[0] {
949 Item::Message { from, to, text, .. } => {
950 assert_eq!(from, "Alice");
951 assert_eq!(to, "Bob");
952 assert_eq!(text, "Hello");
953 }
954 _ => panic!("Expected Message"),
955 }
956 }
957
958 #[test]
960 fn test_multiline_note() {
961 let input = r#"note left of Alice
962Line 1
963Line 2
964end note"#;
965 let result = parse(input).unwrap();
966 assert_eq!(result.items.len(), 1);
967 match &result.items[0] {
968 Item::Note {
969 position,
970 participants,
971 text,
972 } => {
973 assert_eq!(*position, NotePosition::Left);
974 assert_eq!(participants, &["Alice"]);
975 assert_eq!(text, "Line 1\\nLine 2");
976 }
977 _ => panic!("Expected Note"),
978 }
979 }
980
981 #[test]
983 fn test_state() {
984 let result = parse("state over Server: LISTEN").unwrap();
985 assert_eq!(result.items.len(), 1);
986 match &result.items[0] {
987 Item::State { participants, text } => {
988 assert_eq!(participants, &["Server"]);
989 assert_eq!(text, "LISTEN");
990 }
991 _ => panic!("Expected State"),
992 }
993 }
994
995 #[test]
997 fn test_ref() {
998 let result = parse("ref over Alice, Bob: See other diagram").unwrap();
999 assert_eq!(result.items.len(), 1);
1000 match &result.items[0] {
1001 Item::Ref {
1002 participants, text, ..
1003 } => {
1004 assert_eq!(participants, &["Alice", "Bob"]);
1005 assert_eq!(text, "See other diagram");
1006 }
1007 _ => panic!("Expected Ref"),
1008 }
1009 }
1010
1011 #[test]
1012 fn test_ref_input_signal_multiline() {
1013 let input = r#"Alice->ref over Bob, Carol: Input signal
1014line 1
1015line 2
1016end ref-->Alice: Output signal"#;
1017 let result = parse(input).unwrap();
1018 assert_eq!(result.items.len(), 1);
1019 match &result.items[0] {
1020 Item::Ref {
1021 participants,
1022 text,
1023 input_from,
1024 input_label,
1025 output_to,
1026 output_label,
1027 } => {
1028 assert_eq!(participants, &["Bob", "Carol"]);
1029 assert_eq!(text, "line 1\\nline 2");
1030 assert_eq!(input_from.as_deref(), Some("Alice"));
1031 assert_eq!(input_label.as_deref(), Some("Input signal"));
1032 assert_eq!(output_to.as_deref(), Some("Alice"));
1033 assert_eq!(output_label.as_deref(), Some("Output signal"));
1034 }
1035 _ => panic!("Expected Ref"),
1036 }
1037 }
1038
1039 #[test]
1041 fn test_option() {
1042 let result = parse("option footer=none").unwrap();
1043 assert_eq!(result.items.len(), 1);
1044 match &result.items[0] {
1045 Item::DiagramOption { key, value } => {
1046 assert_eq!(key, "footer");
1047 assert_eq!(value, "none");
1048 }
1049 _ => panic!("Expected DiagramOption"),
1050 }
1051 }
1052
1053 #[test]
1055 fn test_quoted_name_with_colon() {
1056 let result = parse(r#"":Alice"->":Bob": Hello"#).unwrap();
1057 assert_eq!(result.items.len(), 1);
1058 match &result.items[0] {
1059 Item::Message { from, to, text, .. } => {
1060 assert_eq!(from, ":Alice");
1061 assert_eq!(to, ":Bob");
1062 assert_eq!(text, "Hello");
1063 }
1064 _ => panic!("Expected Message"),
1065 }
1066 }
1067}