osd_core/
parser.rs

1//! Parser for WebSequenceDiagrams-compatible sequence diagram syntax
2
3use 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/// Parse error
16#[derive(Debug, Clone, thiserror::Error)]
17pub enum ParseError {
18    #[error("Parse error at line {line}: {message}")]
19    SyntaxError { line: usize, message: String },
20}
21
22/// Parse a complete diagram
23pub 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        // Skip empty lines
34        if trimmed.is_empty() {
35            i += 1;
36            continue;
37        }
38
39        // Task 5: Skip comment lines (# ...)
40        if trimmed.starts_with('#') {
41            i += 1;
42            continue;
43        }
44
45        // Task 7: Extended text description (lines starting with space but not empty)
46        if line.starts_with(' ') && !trimmed.is_empty() && !line.starts_with("  ") {
47            // Single space indent is description
48            items.push(Item::Description {
49                text: trimmed.to_string(),
50            });
51            i += 1;
52            continue;
53        }
54
55        // Try parsing title first
56        if let Ok((_, t)) = parse_title(trimmed) {
57            title = Some(t);
58            i += 1;
59            continue;
60        }
61
62        // Task 1: Check for multiline note (note without colon)
63        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        // Task 3: Check for multiline ref (ref over ... without colon on same line ending with text)
85        // Also handles A->ref over B: input ... end ref-->A: output
86        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                // Check for end ref with optional output signal
94                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        // Task 8: Check for parallel { or serial { brace syntax
116        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            // Check if there's content after the opening brace on the same line
121            let after_brace = remaining.trim();
122            if !after_brace.is_empty() && after_brace != "{" {
123                // Parse content after brace if any
124            }
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                    // Recursively parse nested content
141                    if let Some((nested_kind, _)) = parse_brace_block_start(block_line) {
142                        // Handle nested parallel/serial blocks
143                        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        // Regular line parsing
193        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    // Second pass: handle blocks (alt/opt/loop/par/end/else)
208    let items = build_blocks(items)?;
209
210    // Extract options from items
211    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
232/// Check if line starts a multiline note (note without colon)
233fn parse_multiline_note_start(input: &str) -> Option<(NotePosition, Vec<String>)> {
234    let input_lower = input.to_lowercase();
235
236    // Must start with "note" but not have a colon
237    if !input_lower.starts_with("note ") || input.contains(':') {
238        return None;
239    }
240
241    let rest = &input[5..].trim();
242
243    // Determine position
244    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    // Parse participants
255    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
268/// Result of parsing a multiline ref start
269struct RefStartResult {
270    participants: Vec<String>,
271    input_from: Option<String>,
272    input_label: Option<String>,
273}
274
275/// Check if line starts a multiline ref (ref over ... without ending text)
276/// Also handles A->ref over B: label syntax for input signal
277fn 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    // Check for "A->ref over" pattern (input signal)
283    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(); // Keep "ref over ..."
288        }
289    }
290
291    let rest_lower = rest_str.to_lowercase();
292
293    // Must start with "ref over"
294    if !rest_lower.starts_with("ref over ") && !rest_lower.starts_with("ref over") {
295        return None;
296    }
297
298    // Extract part after "ref over "
299    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    // Check for colon (input label for single-line or multiline with label)
307    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    // Parse participants
315    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 there's a label with input_from, this is "A->ref over B: label" format
326    if input_from.is_some() && label.is_some() {
327        input_label = label.map(|s| s.to_string());
328    }
329
330    // For multiline ref, we expect no colon (or the colon case is handled differently)
331    // But if input signal is present with a colon, it's still a valid multiline ref start
332    if label.is_some() && input_from.is_none() {
333        // This is a single-line ref like "ref over A, B: text" - not a multiline start
334        return None;
335    }
336
337    Some(RefStartResult {
338        participants,
339        input_from,
340        input_label,
341    })
342}
343
344/// Parse end ref line with optional output signal
345/// Returns (output_to, output_label)
346fn 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..]; // After "end ref"
355
356    // Check for output signal "-->A: label"
357    if let Some(arrow_pos) = rest.find("-->") {
358        let after_arrow = &rest[arrow_pos + 3..];
359        // Parse "A: label" or just "A"
360        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    // Simple "end ref"
371    Some((None, None))
372}
373
374/// Check if line starts a brace block (parallel { or serial {)
375fn parse_brace_block_start(input: &str) -> Option<(BlockKind, &str)> {
376    let trimmed = input.trim();
377
378    // Check for "parallel {" or "parallel{"
379    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    // Check for "serial {" or "serial{"
387    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
397/// Parse a single line
398fn 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
415/// Parse title
416fn 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
423/// Parse participant declaration: `participant Name` or `actor Name` or `participant "Long Name" as L`
424/// Also supports unquoted names with spaces: `participant OSD Frontend`
425fn 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    // Parse name - check for quoted name first, then unquoted (possibly with spaces)
435    let (remaining, name, alias) = if input.starts_with('"') {
436        // Quoted name
437        let (input, name) = delimited(char('"'), take_until("\""), char('"')).parse(input)?;
438        // Check for alias
439        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        // Unquoted name - take until " as " or end of line
447        // First check if there's an " as " in the remaining input
448        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..]; // skip " as "
452            let (_, alias) = parse_identifier(after_as.trim_start())?;
453            ("", name, Some(alias))
454        } else {
455            // No alias, take the whole remaining line as name
456            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
471/// Parse a name (quoted or unquoted) - Task 6: supports colon in quoted names
472fn parse_name(input: &str) -> IResult<&str, &str> {
473    alt((
474        // Boundary markers for gate/found/lost messages
475        tag("["),
476        tag("]"),
477        // Quoted name (can contain colons, spaces, etc.)
478        delimited(char('"'), take_until("\""), char('"')),
479        // Unquoted identifier
480        parse_identifier,
481    ))
482    .parse(input)
483}
484
485/// Parse an identifier (alphanumeric + underscore)
486fn parse_identifier(input: &str) -> IResult<&str, &str> {
487    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
488}
489
490/// Parse a message: `A->B: text` or `A->>B: text` etc.
491/// Task 6: Now supports quoted names with colons
492/// Also supports unquoted names with spaces: `OSD Frontend->OSD Backend: text`
493fn parse_message(input: &str) -> IResult<&str, Item> {
494    // Arrow patterns to search for (ordered by length, longest first)
495    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    // Find the arrow position and type
505    let mut arrow_pos: Option<(usize, usize, Arrow)> = None; // (start, end, arrow)
506
507    // Check for delayed arrow pattern first: ->(n)
508    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 no delayed arrow, search for regular arrows
522    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    // Split: from is before arrow, rest is after arrow
536    let from = input[..arrow_start].trim();
537    let after_arrow = &input[arrow_end..];
538
539    // Parse modifiers (+, -, *) immediately after arrow
540    let (after_mods, modifiers) = parse_arrow_modifiers(after_arrow)?;
541
542    // Find colon to split to/text, but handle quoted names
543    let (to, text) = if after_mods.trim_start().starts_with('"') {
544        // Quoted "to" name
545        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        // Unquoted "to" name - take until colon or end of line
556        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            // No colon - entire remaining is "to" name
562            (after_mods.trim(), String::new())
563        }
564    };
565
566    // Handle quoted "from" name
567    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
587/// Parse arrow: `->`, `->>`, `-->`, `-->>`, `->(n)`, `<->`, `<-->`
588/// Task 9: Added bidirectional arrow support (though WSD may not use it)
589fn parse_arrow(input: &str) -> IResult<&str, Arrow> {
590    alt((
591        // <--> bidirectional dashed (if needed)
592        value(Arrow::RESPONSE, tag("<-->")),
593        // <-> bidirectional solid (if needed)
594        value(Arrow::SYNC, tag("<->")),
595        // -->> dashed open
596        value(Arrow::RESPONSE_OPEN, tag("-->>")),
597        // --> dashed filled
598        value(Arrow::RESPONSE, tag("-->")),
599        // ->> solid open
600        value(Arrow::SYNC_OPEN, tag("->>")),
601        // ->(n) delayed
602        map(delimited(tag("->("), digit1, char(')')), |n: &str| Arrow {
603            line: LineStyle::Solid,
604            head: ArrowHead::Filled,
605            delay: n.parse().ok(),
606        }),
607        // -> solid filled
608        value(Arrow::SYNC, tag("->")),
609    ))
610    .parse(input)
611}
612
613/// Parse arrow modifiers: `+` (activate), `-` (deactivate), `*` (create)
614fn 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
622/// Parse note: `note left of A: text`, `note right of A: text`, `note over A: text`, `note over A,B: text`
623fn 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    // Parse participants (comma-separated) - support quoted names
645    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
662/// Task 2: Parse state: `state over A: text` or `state over A,B: text`
663fn 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    // Parse participants (comma-separated)
670    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
686/// Task 3: Parse single-line ref: `ref over A,B: text`
687fn 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    // Parse participants (comma-separated)
694    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
714/// Task 4: Parse option: `option key=value`
715fn 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
731/// Parse activate: `activate A`
732fn 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
744/// Parse deactivate: `deactivate A`
745fn 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
757/// Parse destroy: `destroy A`
758fn 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
770/// Parse autonumber: `autonumber` or `autonumber off` or `autonumber 5`
771fn 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
786/// Parse block keywords: alt, opt, loop, par, else, end
787fn parse_block_keyword(input: &str) -> IResult<&str, Item> {
788    alt((parse_block_start, parse_else, parse_end)).parse(input)
789}
790
791/// Parse block start: `alt condition`, `opt condition`, `loop condition`, `par`, `seq`
792fn 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    // Return a marker block that will be processed later
806    Ok((
807        "",
808        Item::Block {
809            kind,
810            label,
811            items: vec![],
812            else_sections: vec![],
813        },
814    ))
815}
816
817/// Parse else: `else condition`
818fn 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    // Return a marker that will be processed during block building
824    Ok((
825        "",
826        Item::Block {
827            kind: BlockKind::Alt, // marker
828            label: format!("__ELSE__{}", label),
829            items: vec![],
830            else_sections: vec![],
831        },
832    ))
833}
834
835/// Parse end (but not "end note" or "end ref")
836fn parse_end(input: &str) -> IResult<&str, Item> {
837    let trimmed = input.trim().to_lowercase();
838    // Don't match "end note" or "end ref" - those are handled separately
839    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, // marker
850            label: "__END__".to_string(),
851            items: vec![],
852            else_sections: vec![],
853        },
854    ))
855}
856
857/// Build block structure from flat list of items
858fn build_blocks(items: Vec<Item>) -> Result<Vec<Item>, ParseError> {
859    use crate::ast::ElseSection;
860
861    let mut result = Vec::new();
862    // Stack entry: (kind, label, items, else_sections, current_else_items, current_else_label, in_else_branch)
863    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                // End of block
878                if let Some(mut entry) = stack.pop() {
879                    // If we were in an else branch, finalize it
880                    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                // Else marker - extract the else label
905                let else_label_text = label.strip_prefix("__ELSE__").unwrap_or("").to_string();
906                if let Some(entry) = stack.last_mut() {
907                    // If we were already in an else branch, save the current one
908                    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                    // Start new else branch
915                    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                // Check if this is a completed block (parallel/serial with items already)
932                if matches!(kind, BlockKind::Parallel | BlockKind::Serial) || !items.is_empty() {
933                    // Already a complete block, add directly
934                    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                    // Block start marker
951                    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                // Regular item
964                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    // Task 5: Comment test
1062    #[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    // Task 1: Multiline note test
1077    #[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    // Task 2: State test
1100    #[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    // Task 3: Ref test
1114    #[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    // Task 4: Option test
1158    #[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    // Task 6: Quoted name with colon test
1172    #[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: Participant names with spaces (WSD compatibility)
1187    #[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        // Check participant declarations
1197        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        // Check messages with spaces in participant names
1215        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: Participant declaration with spaces and alias
1234    #[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}