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_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        // Regular line parsing
195        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    // Second pass: handle blocks (alt/opt/loop/par/end/else)
210    let items = build_blocks(items)?;
211
212    // Extract options from items
213    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
234/// Check if line starts a multiline note (note without colon)
235fn parse_multiline_note_start(input: &str) -> Option<(NotePosition, Vec<String>)> {
236    let input_lower = input.to_lowercase();
237
238    // Must start with "note" but not have a colon
239    if !input_lower.starts_with("note ") || input.contains(':') {
240        return None;
241    }
242
243    let rest = &input[5..].trim();
244
245    // Determine position
246    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    // Parse participants
257    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
270/// Result of parsing a multiline ref start
271struct RefStartResult {
272    participants: Vec<String>,
273    input_from: Option<String>,
274    input_label: Option<String>,
275}
276
277/// Check if line starts a multiline ref (ref over ... without ending text)
278/// Also handles A->ref over B: label syntax for input signal
279fn 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    // Check for "A->ref over" pattern (input signal)
285    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(); // Keep "ref over ..."
290        }
291    }
292
293    let rest_lower = rest_str.to_lowercase();
294
295    // Must start with "ref over"
296    if !rest_lower.starts_with("ref over ") && !rest_lower.starts_with("ref over") {
297        return None;
298    }
299
300    // Extract part after "ref over "
301    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    // Check for colon (input label for single-line or multiline with label)
309    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    // Parse participants
317    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 there's a label with input_from, this is "A->ref over B: label" format
328    if input_from.is_some() && label.is_some() {
329        input_label = label.map(|s| s.to_string());
330    }
331
332    // For multiline ref, we expect no colon (or the colon case is handled differently)
333    // But if input signal is present with a colon, it's still a valid multiline ref start
334    if label.is_some() && input_from.is_none() {
335        // This is a single-line ref like "ref over A, B: text" - not a multiline start
336        return None;
337    }
338
339    Some(RefStartResult {
340        participants,
341        input_from,
342        input_label,
343    })
344}
345
346/// Parse end ref line with optional output signal
347/// Returns (output_to, output_label)
348fn 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..]; // After "end ref"
357
358    // Check for output signal "-->A: label"
359    if let Some(arrow_pos) = rest.find("-->") {
360        let after_arrow = &rest[arrow_pos + 3..];
361        // Parse "A: label" or just "A"
362        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    // Simple "end ref"
373    Some((None, None))
374}
375
376/// Check if line starts a brace block (parallel { or serial {)
377fn parse_brace_block_start(input: &str) -> Option<(BlockKind, &str)> {
378    let trimmed = input.trim();
379
380    // Check for "parallel {" or "parallel{"
381    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    // Check for "serial {" or "serial{"
389    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
399/// Parse a single line
400fn 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
417/// Parse title
418fn 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
425/// Parse participant declaration: `participant Name` or `actor Name` or `participant "Long Name" as L`
426fn 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    // Parse name (possibly quoted)
436    let (input, name) = parse_name(input)?;
437
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
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
455/// Parse a name (quoted or unquoted) - Task 6: supports colon in quoted names
456fn parse_name(input: &str) -> IResult<&str, &str> {
457    alt((
458        // Quoted name (can contain colons, spaces, etc.)
459        delimited(char('"'), take_until("\""), char('"')),
460        // Unquoted identifier
461        parse_identifier,
462    ))
463    .parse(input)
464}
465
466/// Parse an identifier (alphanumeric + underscore)
467fn parse_identifier(input: &str) -> IResult<&str, &str> {
468    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
469}
470
471/// Parse a message: `A->B: text` or `A->>B: text` etc.
472/// Task 6: Now supports quoted names with colons
473fn 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
496/// Parse arrow: `->`, `->>`, `-->`, `-->>`, `->(n)`, `<->`, `<-->`
497/// Task 9: Added bidirectional arrow support (though WSD may not use it)
498fn parse_arrow(input: &str) -> IResult<&str, Arrow> {
499    alt((
500        // <--> bidirectional dashed (if needed)
501        value(Arrow::RESPONSE, tag("<-->")),
502        // <-> bidirectional solid (if needed)
503        value(Arrow::SYNC, tag("<->")),
504        // -->> dashed open
505        value(Arrow::RESPONSE_OPEN, tag("-->>")),
506        // --> dashed filled
507        value(Arrow::RESPONSE, tag("-->")),
508        // ->> solid open
509        value(Arrow::SYNC_OPEN, tag("->>")),
510        // ->(n) delayed
511        map(delimited(tag("->("), digit1, char(')')), |n: &str| Arrow {
512            line: LineStyle::Solid,
513            head: ArrowHead::Filled,
514            delay: n.parse().ok(),
515        }),
516        // -> solid filled
517        value(Arrow::SYNC, tag("->")),
518    ))
519    .parse(input)
520}
521
522/// Parse arrow modifiers: `+` (activate), `-` (deactivate), `*` (create)
523fn 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
531/// Parse note: `note left of A: text`, `note right of A: text`, `note over A: text`, `note over A,B: text`
532fn 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    // Parse participants (comma-separated) - support quoted names
554    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
571/// Task 2: Parse state: `state over A: text` or `state over A,B: text`
572fn 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    // Parse participants (comma-separated)
579    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
595/// Task 3: Parse single-line ref: `ref over A,B: text`
596fn 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    // Parse participants (comma-separated)
603    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
623/// Task 4: Parse option: `option key=value`
624fn 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
640/// Parse activate: `activate A`
641fn 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
653/// Parse deactivate: `deactivate A`
654fn 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
666/// Parse destroy: `destroy A`
667fn 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
679/// Parse autonumber: `autonumber` or `autonumber off` or `autonumber 5`
680fn 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
695/// Parse block keywords: alt, opt, loop, par, else, end
696fn parse_block_keyword(input: &str) -> IResult<&str, Item> {
697    alt((parse_block_start, parse_else, parse_end)).parse(input)
698}
699
700/// Parse block start: `alt condition`, `opt condition`, `loop condition`, `par`, `seq`
701fn 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    // Return a marker block that will be processed later
715    Ok((
716        "",
717        Item::Block {
718            kind,
719            label,
720            items: vec![],
721            else_items: None,
722            else_label: None,
723        },
724    ))
725}
726
727/// Parse else: `else condition`
728fn 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    // Return a marker that will be processed during block building
734    Ok((
735        "",
736        Item::Block {
737            kind: BlockKind::Alt, // marker
738            label: format!("__ELSE__{}", label),
739            items: vec![],
740            else_items: None,
741            else_label: None,
742        },
743    ))
744}
745
746/// Parse end (but not "end note" or "end ref")
747fn parse_end(input: &str) -> IResult<&str, Item> {
748    let trimmed = input.trim().to_lowercase();
749    // Don't match "end note" or "end ref" - those are handled separately
750    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, // marker
761            label: "__END__".to_string(),
762            items: vec![],
763            else_items: None,
764            else_label: None,
765        },
766    ))
767}
768
769/// Build block structure from flat list of items
770fn build_blocks(items: Vec<Item>) -> Result<Vec<Item>, ParseError> {
771    let mut result = Vec::new();
772    // Stack: (kind, label, items, else_items, in_else_branch, else_label)
773    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                // End of block
779                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                            // In else branch
790                            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                // Else marker - extract the else label
801                let else_label_text = label.strip_prefix("__ELSE__").unwrap_or("").to_string();
802                if let Some(parent) = stack.last_mut() {
803                    parent.4 = true; // Switch to else branch
804                    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                // Check if this is a completed block (parallel/serial with items already)
820                if matches!(kind, BlockKind::Parallel | BlockKind::Serial) || !items.is_empty() {
821                    // Already a complete block, add directly
822                    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                    // Block start marker
840                    stack.push((*kind, label.clone(), Vec::new(), None, false, None));
841                }
842            }
843            _ => {
844                // Regular item
845                if let Some(parent) = stack.last_mut() {
846                    if parent.4 {
847                        // In else branch
848                        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    // Task 5: Comment test
944    #[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    // Task 1: Multiline note test
959    #[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    // Task 2: State test
982    #[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    // Task 3: Ref test
996    #[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    // Task 4: Option test
1040    #[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    // Task 6: Quoted name with colon test
1054    #[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}