merman-core 0.8.0

Mermaid parser + semantic model (headless; parity-focused).
Documentation
use crate::SourceSpan;

mod actor;
mod scanner;

use scanner::SequenceScanner;

#[cfg(test)]
use actor::HALF_ARROW_TYPES;

#[derive(Debug, Clone)]
pub(crate) enum Tok {
    Newline,

    SequenceDiagram,
    Participant,
    ActorKw,
    Create,
    Destroy,
    As,

    Box,
    End,

    Loop,
    Rect,
    Opt,
    Alt,
    Else,
    Par,
    ParOver,
    And,
    Critical,
    Option,
    Break,

    Note,
    LeftOf,
    RightOf,
    Over,

    Links,
    Link,
    Properties,
    Details,

    Autonumber,
    Off,

    Activate,
    Deactivate,

    Comma,
    Plus,
    Minus,
    Central,

    Num(f64),
    Actor(String),
    Text(String),
    RestOfLine(String),
    SignalType(i32),
    Config(String),

    Title(String),
    CompatTitle(String),
    AccTitle(String),
    AccDescr(String),
    AccDescrMultiline(String),
}

#[derive(Debug, Clone, thiserror::Error)]
#[error("{message}")]
pub(crate) struct LexError {
    pub message: String,
    pub span: SourceSpan,
}

impl crate::error::ParseErrorSourceSpan for LexError {
    fn source_span(&self) -> Option<crate::SourceSpan> {
        Some(self.span)
    }
}

pub(super) struct Lexer<'input> {
    scanner: SequenceScanner<'input>,
}

impl<'input> Lexer<'input> {
    pub(super) fn new(input: &'input str) -> Self {
        Self {
            scanner: SequenceScanner::new(input),
        }
    }

    pub(super) fn position(&self) -> usize {
        self.scanner.position()
    }
}

impl Iterator for Lexer<'_> {
    type Item = std::result::Result<(usize, Tok, usize), LexError>;

    fn next(&mut self) -> Option<Self::Item> {
        self.scanner.next_token()
    }
}

#[cfg(test)]
mod tests {
    use super::{HALF_ARROW_TYPES, Lexer, Tok};

    fn token_trace(input: &str) -> Vec<(usize, &'static str, usize)> {
        Lexer::new(input)
            .map(|event| {
                let (start, token, end) = event.expect("sequence token");
                let name = match token {
                    Tok::AccDescrMultiline(_) => "accDescrMultiline",
                    Tok::Newline => "newline",
                    Tok::Participant => "participant",
                    _ => "other",
                };
                (start, name, end)
            })
            .collect()
    }

    fn boundary_after_accessibility(input: &str) -> (usize, &'static str, usize) {
        let trace = token_trace(input);
        let accessibility = trace
            .iter()
            .position(|(_, name, _)| *name == "accDescrMultiline")
            .expect("multiline accessibility token");
        trace[accessibility + 1]
    }

    #[test]
    fn multiline_accessibility_only_synthesizes_required_statement_boundaries() {
        let ordinary = "sequenceDiagram\naccDescr {desc}\nparticipant A";
        let physical_newline = ordinary.find("}\n").expect("closing brace") + 1;
        assert_eq!(
            boundary_after_accessibility(ordinary),
            (physical_newline, "newline", physical_newline + 1)
        );

        let same_line = "sequenceDiagram\naccDescr {desc} participant A";
        let closing = same_line.find('}').expect("closing brace") + 1;
        assert_eq!(
            boundary_after_accessibility(same_line),
            (closing, "newline", closing)
        );

        let eof = "sequenceDiagram\naccDescr {desc}";
        assert_eq!(
            boundary_after_accessibility(eof),
            (eof.len(), "newline", eof.len())
        );
    }

    #[test]
    fn lexes_all_upstream_half_arrow_variants() {
        for (arrow, expected_type) in HALF_ARROW_TYPES {
            for input in [
                format!("A {arrow} B: message"),
                format!("A{arrow}B: message"),
                format!("顧客{arrow}サーバー: message"),
            ] {
                let signal_types: Vec<_> = Lexer::new(&input)
                    .map(|event| event.expect("sequence token").1)
                    .filter_map(|token| match token {
                        Tok::SignalType(signal_type) => Some(signal_type),
                        _ => None,
                    })
                    .collect();

                assert_eq!(signal_types, vec![expected_type], "{input}");
            }
        }
    }
}