tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use std::sync::Arc;

use super::super::diagram::{
    ParsedDiagram, SEQUENCE_NOTE_LABEL_PREFIX, SequenceMessageSpec, SequenceParticipantSpec,
    SequenceSpec,
};
use super::{MermaidParseError, parse_labelled_pair, significant_lines, strip_quotes};

pub(crate) fn parse(src: &str) -> Result<ParsedDiagram, MermaidParseError> {
    let mut participants = Vec::<SequenceParticipantSpec>::new();
    let mut messages = Vec::new();
    for (line_no, line) in significant_lines(src).into_iter().skip(1) {
        if is_ignored_sequence_structure_line(&line) {
            continue;
        }
        if let Some(rest) = line
            .strip_prefix("participant ")
            .or_else(|| line.strip_prefix("actor "))
        {
            let actor = line.starts_with("actor ");
            let (id, label) = rest
                .split_once(" as ")
                .map(|(id, label)| (id.trim(), strip_quotes(label)))
                .unwrap_or_else(|| (rest.trim(), rest.trim()));
            push_participant(&mut participants, id, label, actor);
            continue;
        }
        if let Some((actors, text)) = parse_note(&line, line_no)? {
            for actor in &actors {
                push_participant(&mut participants, actor, actor, false);
            }
            let from = actors.first().copied().unwrap_or_default();
            let to = actors.last().copied().unwrap_or(from);
            messages.push(SequenceMessageSpec {
                from: Arc::from(from),
                to: Arc::from(to),
                label: Arc::from(format!("{SEQUENCE_NOTE_LABEL_PREFIX}{text}")),
                dashed: false,
                open_arrow: true,
            });
            continue;
        }
        let arrow = ["-->>", "->>", "-->", "->"]
            .into_iter()
            .find(|a| line.contains(a));
        let Some(arrow) = arrow else {
            return Err(MermaidParseError::new(
                "expected sequence message",
                Some(line_no),
            ));
        };
        let (from, rest) = line.split_once(arrow).unwrap();
        let (to, label) = parse_labelled_pair(rest, ":", line_no)?;
        push_participant(&mut participants, from.trim(), from.trim(), false);
        push_participant(&mut participants, to.trim(), to.trim(), false);
        messages.push(SequenceMessageSpec {
            from: Arc::from(from.trim()),
            to: Arc::from(to.trim()),
            label: Arc::from(label),
            dashed: arrow.starts_with("--"),
            open_arrow: !arrow.ends_with(">>"),
        });
    }
    if messages.is_empty() && participants.is_empty() {
        return Err(MermaidParseError::new("empty sequence diagram", None));
    }
    Ok(ParsedDiagram::Sequence(SequenceSpec {
        participants,
        messages,
    }))
}

fn is_ignored_sequence_structure_line(line: &str) -> bool {
    line == "end"
        || line == "else"
        || line.starts_with("loop ")
        || line.starts_with("alt ")
        || line.starts_with("opt ")
        || line.starts_with("par ")
        || line.starts_with("and ")
        || line.starts_with("rect ")
        || line.starts_with("critical ")
        || line.starts_with("break ")
        || line.starts_with("activate ")
        || line.starts_with("deactivate ")
}

fn push_participant(
    participants: &mut Vec<SequenceParticipantSpec>,
    id: &str,
    label: &str,
    actor: bool,
) {
    if !participants.iter().any(|p| p.id.as_ref() == id) {
        participants.push(SequenceParticipantSpec {
            id: Arc::from(id),
            label: Arc::from(label),
            actor,
        });
    }
}

fn parse_note(line: &str, line_no: usize) -> Result<Option<(Vec<&str>, &str)>, MermaidParseError> {
    let Some(rest) = line.strip_prefix("Note ") else {
        return Ok(None);
    };
    let Some(actor_text) = rest
        .strip_prefix("over ")
        .or_else(|| rest.strip_prefix("left of "))
        .or_else(|| rest.strip_prefix("right of "))
    else {
        return Err(MermaidParseError::new(
            "expected `Note over`, `Note left of`, or `Note right of`",
            Some(line_no),
        ));
    };
    let (actors, text) = parse_labelled_pair(actor_text, ":", line_no)?;
    let actors = actors
        .split(',')
        .map(str::trim)
        .filter(|actor| !actor.is_empty())
        .collect::<Vec<_>>();
    if actors.is_empty() {
        return Err(MermaidParseError::new("expected note actor", Some(line_no)));
    }
    Ok(Some((actors, strip_quotes(text))))
}