merman-core 0.8.0-alpha.4

Mermaid parser + semantic model (headless; parity-focused).
Documentation
include_checked_in_lalrpop_parser!(
    #[allow(
        clippy::empty_line_after_outer_attr,
        clippy::type_complexity,
        clippy::result_large_err
    )]
    sequence_grammar,
    "sequence_grammar.rs"
);

// Mermaid 11.15.x sequence diagram constants (SequenceDB.LINETYPE / PLACEMENT).
const LINETYPE_NOTE: i32 = 2;
const LINETYPE_LOOP_START: i32 = 10;
const LINETYPE_LOOP_END: i32 = 11;
const LINETYPE_ALT_START: i32 = 12;
const LINETYPE_ALT_ELSE: i32 = 13;
const LINETYPE_ALT_END: i32 = 14;
const LINETYPE_OPT_START: i32 = 15;
const LINETYPE_OPT_END: i32 = 16;
const LINETYPE_ACTIVE_START: i32 = 17;
const LINETYPE_ACTIVE_END: i32 = 18;
const LINETYPE_PAR_START: i32 = 19;
const LINETYPE_PAR_AND: i32 = 20;
const LINETYPE_PAR_END: i32 = 21;
const LINETYPE_RECT_START: i32 = 22;
const LINETYPE_RECT_END: i32 = 23;
const LINETYPE_AUTONUMBER: i32 = 26;
const LINETYPE_CRITICAL_START: i32 = 27;
const LINETYPE_CRITICAL_OPTION: i32 = 28;
const LINETYPE_CRITICAL_END: i32 = 29;
const LINETYPE_BREAK_START: i32 = 30;
const LINETYPE_BREAK_END: i32 = 31;
const LINETYPE_PAR_OVER_START: i32 = 32;
const LINETYPE_CENTRAL_CONNECTION: i32 = 59;
const LINETYPE_CENTRAL_CONNECTION_REVERSE: i32 = 60;
const LINETYPE_CENTRAL_CONNECTION_DUAL: i32 = 61;

const PLACEMENT_LEFT_OF: i32 = 0;
const PLACEMENT_RIGHT_OF: i32 = 1;
const PLACEMENT_OVER: i32 = 2;

mod ast;
mod db;
mod lexer;
mod parse;
mod render_model;

use ast::Action;
pub(crate) use lexer::{LexError, Tok};

pub(crate) use parse::parse_sequence_json_and_editor_facts;
pub(crate) use parse::{parse_sequence, parse_sequence_model_for_render};
#[cfg(test)]
pub(crate) use parse::{
    reset_sequence_syntax_construction_count, sequence_syntax_construction_count,
};
pub(crate) use render_model::render_model_to_compat_json;
pub use render_model::{
    SequenceActor, SequenceAutonumber, SequenceBox, SequenceDiagramRenderModel, SequenceMessage,
    SequenceMessagePayload, SequenceNote,
};

#[cfg(test)]
mod tests {
    use super::render_model_to_compat_json;
    use crate::{Engine, ParseOptions, RenderSemanticModel};

    #[test]
    fn parser_preserves_bracketed_block_titles_for_the_renderer() {
        let parsed = Engine::new()
            .parse_diagram_for_render_model_sync(
                r#"sequenceDiagram
    par [Action 1]
        Alice->>Bob: First
    and [Action 2]
        Bob-->>Alice: Second
    end"#,
                ParseOptions::strict(),
            )
            .expect("parse should succeed")
            .expect("sequence diagram should be detected");
        let RenderSemanticModel::Sequence(model) = parsed.model() else {
            panic!("expected typed Sequence render model");
        };

        assert_eq!(model.messages[0].message_text(), "[Action 1]");
        assert_eq!(model.messages[2].message_text(), "[Action 2]");
    }

    #[test]
    fn typed_render_model_projects_exact_compatibility_json() {
        let source = r#"sequenceDiagram
title: Delivery
accTitle: Delivery sequence
autonumber 10 5
participant Alice
actor Bob
box rgb(240,240,240) Team
participant Carol
end
Alice->>Bob: Request
Note over Alice,Bob: Review
create participant Worker
Bob->>Worker: Start
destroy Worker
Worker-->>Bob: Done"#;
        let engine = crate::Engine::new();
        let compat = engine
            .parse_diagram_sync(source, crate::ParseOptions::strict())
            .unwrap()
            .unwrap();
        let typed = engine
            .parse_diagram_for_render_model_sync(source, crate::ParseOptions::strict())
            .unwrap()
            .unwrap();
        let crate::RenderSemanticModel::Sequence(model) = typed.model() else {
            panic!("expected Sequence render model");
        };

        assert_eq!(
            render_model_to_compat_json(model, typed.metadata()).unwrap(),
            compat.model
        );
    }
}