merman-core 0.8.0-alpha.5

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

#[test]
fn parse_cynefin_domains_transitions_and_editor_facts() {
    let engine = Engine::new();
    let text = r#"cynefin-beta
  title Team Practices
  accTitle: Cynefin for team practices
  accDescr: A diagram showing the five Cynefin domains
  complex
    "Retrospectives"
    "Pair programming"
  complicated
    "Code review"
  complex --> complicated : "Pattern emerges"
  complicated --> complicated : "Self-loop"
"#;

    let parsed = engine
        .parse_diagram_snapshot_sync(text)
        .unwrap()
        .expect("cynefin parses");

    assert_eq!(parsed.metadata().diagram_type, "cynefin");
    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot"),
        &json!({
            "type": "cynefin",
            "title": "Team Practices",
            "accTitle": "Cynefin for team practices",
            "accDescr": "A diagram showing the five Cynefin domains",
            "domains": [
                {
                    "name": "complex",
                    "items": [
                        { "label": "Retrospectives" },
                        { "label": "Pair programming" }
                    ]
                },
                {
                    "name": "complicated",
                    "items": [
                        { "label": "Code review" }
                    ]
                }
            ],
            "transitions": [
                {
                    "from": "complex",
                    "to": "complicated",
                    "label": "Pattern emerges"
                }
            ]
        })
    );

    let ParsedEditorFacts::Available(facts) = parsed.editor_facts() else {
        panic!("cynefin should expose editor facts");
    };
    assert!(
        facts
            .directive_prefixes
            .iter()
            .any(|prefix| prefix == "title")
    );
    assert!(facts.symbols.iter().any(|symbol| {
        symbol.name == "complex"
            && symbol.detail.as_deref() == Some("cynefin domain")
            && symbol.selection
                == SourceSpan::new(
                    text.find("complex").expect("complex domain"),
                    text.find("complex").expect("complex domain") + "complex".len(),
                )
    }));
    assert!(facts.symbols.iter().any(|symbol| {
        symbol.name == "Retrospectives" && symbol.detail.as_deref() == Some("cynefin domain item")
    }));
    assert!(facts.diagnostics.iter().any(|diagnostic| {
        diagnostic
            .message
            .contains("self-loop transition on domain \"complicated\" is skipped")
    }));
}

#[test]
fn parse_cynefin_accepts_colon_header_comments_single_quotes_and_escapes() {
    let engine = Engine::new();
    let text = "cynefin-beta:\n  %% comment\n  clear\n    'Known \\'good\\' practice %% kept'\n  clear --> chaotic %% transition comment\n";
    let parsed = engine
        .parse_diagram_sync(text, ParseOptions::strict())
        .unwrap()
        .expect("cynefin parses");

    assert_eq!(parsed.meta.diagram_type, "cynefin");
    assert_eq!(parsed.model["domains"][0]["name"], json!("clear"));
    assert_eq!(
        parsed.model["domains"][0]["items"][0]["label"],
        json!("Known 'good' practice %% kept")
    );
    assert_eq!(
        parsed.model["transitions"][0],
        json!({
            "from": "clear",
            "to": "chaotic"
        })
    );
}

#[test]
fn parse_cynefin_accepts_header_domains_and_items_on_the_same_line() {
    let engine = Engine::new();
    let text = r#"cynefin-beta complex "A" "B" complicated "C"
complicated --> clear : "Standardize""#;
    let parsed = engine
        .parse_diagram_snapshot_sync(text)
        .unwrap()
        .expect("cynefin parses tokens separated only by hidden whitespace");

    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot")["domains"],
        json!([
            {
                "name": "complex",
                "items": [{ "label": "A" }, { "label": "B" }]
            },
            {
                "name": "complicated",
                "items": [{ "label": "C" }]
            }
        ])
    );
    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot")["transitions"],
        json!([{
            "from": "complicated",
            "to": "clear",
            "label": "Standardize"
        }])
    );

    let ParsedEditorFacts::Available(facts) = parsed.editor_facts() else {
        panic!("cynefin should expose editor facts");
    };
    assert!(facts.symbols.iter().any(|symbol| {
        symbol.name == "A" && symbol.detail.as_deref() == Some("cynefin domain item")
    }));
    assert!(facts.symbols.iter().any(|symbol| {
        symbol.name == "complicated" && symbol.detail.as_deref() == Some("cynefin domain")
    }));
}

#[test]
fn parse_cynefin_accepts_multiline_accessibility_directive_after_inline_header() {
    let engine = Engine::new();
    let text = r#"cynefin-beta accDescr {
  Inline header description
}
complex "A""#;
    let parsed = engine
        .parse_diagram_sync(text, ParseOptions::strict())
        .unwrap()
        .expect("cynefin parses inline header before multiline common directive");

    assert_eq!(parsed.model["accDescr"], json!("Inline header description"));
    assert_eq!(parsed.model["domains"][0]["name"], json!("complex"));
    assert_eq!(parsed.model["domains"][0]["items"][0]["label"], json!("A"));
}

#[test]
fn parse_cynefin_unescapes_common_strings_like_langium() {
    let engine = Engine::new();
    let parsed = engine
        .parse_diagram_sync(
            r#"cynefin-beta
  complex
    "Probe\nsense\trespond\bnow"
  complex --> clear : 'Move\rnow\vsoon\0'
"#,
            ParseOptions::strict(),
        )
        .unwrap()
        .expect("cynefin parses escaped strings");

    assert_eq!(
        parsed.model["domains"][0]["items"][0]["label"],
        json!("Probensensetrespondbnow")
    );
    assert_eq!(
        parsed.model["transitions"][0]["label"],
        json!("Movernowvsoon0")
    );
}

#[test]
fn parse_cynefin_transition_labels_follow_javascript_truthiness() {
    let engine = Engine::new();
    let parsed = engine
        .parse_diagram_sync(
            "cynefin-beta\n  complex\n  complicated\n  clear\n  complex --> complicated : \"\"\n  complicated --> clear : \"   \"\n",
            ParseOptions::strict(),
        )
        .unwrap()
        .expect("cynefin parses");

    assert!(parsed.model["transitions"][0].get("label").is_none());
    assert_eq!(parsed.model["transitions"][1]["label"], json!("   "));
}

#[test]
fn parse_cynefin_normalizes_common_fields_and_multiline_acc_descr() {
    let engine = Engine::new();
    let parsed = engine
        .parse_diagram_snapshot_sync(
            "cynefin-beta\n  title    Team\t\tPractices   Map  \n  accTitle:   Team\t\tMap  \n  accDescr {\n    First    line\n\n      Second\t\tline   \n  }\n  clear\n",
        )
        .unwrap()
        .expect("cynefin parses multiline accessibility metadata");

    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot")["title"],
        json!("Team Practices Map")
    );
    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot")["accTitle"],
        json!("Team Map")
    );
    assert_eq!(
        parsed
            .outcome()
            .parsed_model()
            .expect("expected parsed snapshot")["accDescr"],
        json!("First line\nSecond line")
    );

    let ParsedEditorFacts::Available(facts) = parsed.editor_facts() else {
        panic!("cynefin should expose editor facts");
    };
    assert!(facts.symbols.iter().any(|symbol| {
        symbol.detail.as_deref() == Some("cynefin accessibility description")
            && symbol.name == "First line\nSecond line"
    }));
}

#[test]
fn parse_cynefin_duplicate_domain_replaces_prior_items_like_upstream_map_set() {
    let engine = Engine::new();
    let parsed = engine
        .parse_diagram_sync(
            "cynefin-beta\n  complex\n    \"Old\"\n  complex\n    \"New\"\n",
            ParseOptions::strict(),
        )
        .unwrap()
        .expect("cynefin parses");

    assert_eq!(
        parsed.model["domains"],
        json!([
            {
                "name": "complex",
                "items": [
                    { "label": "New" }
                ]
            }
        ])
    );
}

#[test]
fn parse_cynefin_render_model_uses_typed_model() {
    let engine = Engine::new();
    let parsed = engine
        .parse_diagram_for_render_model_sync("cynefin-beta\n  complex\n", ParseOptions::strict())
        .unwrap()
        .expect("cynefin render model parses");

    assert_eq!(parsed.metadata().diagram_type, "cynefin");
    assert_eq!(parsed.model().kind(), "cynefin");
    assert!(parsed.model().supports_diagram_type("cynefin"));
    let RenderSemanticModel::Cynefin(model) = parsed.model() else {
        panic!("expected typed cynefin render model");
    };
    assert_eq!(model.domains[0].name, "complex");
}