merman 0.9.0

Parity-focused, headless Rust implementation of Mermaid for parsing, layout, and rendering.
Documentation
#![cfg(all(feature = "svg", feature = "all-diagrams"))]

use merman::svg::RenderResourcePolicy;
use merman::{
    OperationControl, ParseOptions, RenderOutput, RenderTarget, Renderer, SvgEnvironment,
    SvgRequest,
};
use std::fmt::Write as _;
use std::fs::{self, File};
use std::io::{self, Write as _};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

const CASE_ENV: &str = "MERMAN_FACADE_BOUNDED_LIFECYCLE_CASE";
const WORKER_STACK_BYTES: usize = 2 * 1024 * 1024;
const CHILD_TIMEOUT: Duration = Duration::from_secs(60);
const LOCAL_DEPTH: usize = 256;

fn source(case: &str) -> String {
    match case {
        "railroad" => format!(
            "railroad-beta\nentry = {}terminal(\"a\"){} ;\n",
            "optional(".repeat(LOCAL_DEPTH),
            ")".repeat(LOCAL_DEPTH)
        ),
        "railroadEbnf" => format!(
            "railroad-ebnf-beta\nentry = \"a\"{} ;\n",
            "?".repeat(LOCAL_DEPTH)
        ),
        "railroadAbnf" => format!(
            "railroad-abnf-beta\nentry = {}\"a\"{} ;\n",
            "[".repeat(LOCAL_DEPTH),
            "]".repeat(LOCAL_DEPTH)
        ),
        "railroadPeg" => format!(
            "railroad-peg-beta\nentry <- {}\"a\"{} ;\n",
            "(".repeat(LOCAL_DEPTH),
            ")?".repeat(LOCAL_DEPTH)
        ),
        "zenuml" => format!(
            "zenuml\n{}A.call()\n{}",
            "opt {\n".repeat(LOCAL_DEPTH),
            "}\n".repeat(LOCAL_DEPTH)
        ),
        "treeView" => {
            let mut source = String::from("treeView-beta\n");
            for depth in 0..LOCAL_DEPTH {
                writeln!(source, "{}\"n{depth}\"", " ".repeat(depth)).unwrap();
            }
            source
        }
        "usecase" => format!(
            "usecase-beta\njson Data@{{\"value\":{}1{}}}\n",
            "[".repeat(126),
            "]".repeat(126)
        ),
        _ => panic!("unknown bounded facade case {case}"),
    }
}

fn marker(phase: &str, status: &str) {
    println!("phase={phase} status={status}");
    io::stdout().flush().expect("flush bounded facade marker");
}

fn run_worker(case: &str) {
    let source = source(case);
    println!(
        "case={case} source_bytes={} worker_stack_bytes={WORKER_STACK_BYTES} path=Renderer.prepare_semantic+SemanticArtifact.render profile=unbounded-for-trusted-input build_mode={}",
        source.len(),
        if cfg!(debug_assertions) {
            "debug"
        } else {
            "release"
        }
    );
    let policy = RenderResourcePolicy::unbounded_for_trusted_input();
    let renderer = Renderer::new()
        .with_parse_options(ParseOptions::strict())
        .with_resource_policy(*policy.input_policy());
    marker("parse", "begin");
    let semantic = renderer
        .prepare_semantic(&source, OperationControl::new())
        .expect("accepted bounded source prepares through the facade")
        .unwrap();
    assert_eq!(semantic.diagram_type(), case);
    marker("parse", "done");
    marker("export", "begin");
    let json = semantic
        .compatibility_json()
        .expect("accepted bounded facade projection");
    let mut encoded = Vec::new();
    json.write_json(&mut encoded)
        .expect("bounded facade iterative export");
    assert!(!encoded.is_empty());
    let cloned = json.clone();
    assert_eq!(cloned, json);
    drop(cloned);
    drop(json);
    marker("export", "done");
    marker("render", "begin");
    let request = SvgRequest {
        environment: SvgEnvironment::deterministic().with_resource_policy(policy),
        ..Default::default()
    };
    let output = semantic
        .render(RenderTarget::Svg(request))
        .expect("maximum accepted bounded model renders native SVG");
    let RenderOutput::Svg(Some(svg)) = output else {
        panic!("bounded family supports native SVG")
    };
    assert!(svg.svg().contains("<svg"));
    println!("svg_bytes={}", svg.svg().len());
    marker("render", "done");
    drop(svg);
    marker("drop", "done");
    drop(
        renderer
            .prepare_semantic(
                "railroad-beta\nafter = terminal(\"ok\") ;\n",
                OperationControl::new(),
            )
            .unwrap()
            .unwrap(),
    );
    marker("after-small-operation", "done");
}

fn run_child(case: &str) {
    let started = Instant::now();
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("diagnostic timestamp follows the Unix epoch")
        .as_nanos();
    let directory = std::env::temp_dir().join(format!(
        "merman-facade-bounded-{}-{case}-{timestamp}",
        std::process::id()
    ));
    fs::create_dir(&directory).unwrap_or_else(|error| {
        panic!(
            "create bounded facade diagnostic directory {}: {error}",
            directory.display()
        )
    });
    let stdout_path = directory.join("stdout.txt");
    let stderr_path = directory.join("stderr.txt");
    let mut child = Command::new(std::env::current_exe().unwrap())
        .args(["--exact", "bounded_facade_lifecycle_child", "--nocapture"])
        .env(CASE_ENV, case)
        .stdout(Stdio::from(File::create(&stdout_path).unwrap()))
        .stderr(Stdio::from(File::create(&stderr_path).unwrap()))
        .spawn()
        .expect("spawn bounded facade lifecycle child");
    let (status, timed_out, process_error) = loop {
        match child.try_wait() {
            Ok(Some(status)) => break (status, false, None),
            Ok(None) => {}
            Err(error) => {
                let _ = child.kill();
                break (
                    child.wait().expect("reap failed facade child"),
                    false,
                    Some(error.to_string()),
                );
            }
        }
        if started.elapsed() >= CHILD_TIMEOUT {
            let error = child.kill().err().map(|error| error.to_string());
            break (
                child.wait().expect("reap timed-out facade child"),
                true,
                error,
            );
        }
        std::thread::sleep(Duration::from_millis(10));
    };
    let stdout = fs::read_to_string(&stdout_path).unwrap();
    let stderr = fs::read_to_string(&stderr_path).unwrap();
    let summary = format!(
        "case={case} status={status} timed_out={timed_out} elapsed_ms={} process_error={process_error:?}",
        started.elapsed().as_millis()
    );
    fs::write(directory.join("status.txt"), &summary).unwrap();
    println!(
        "{summary}\ndiagnostics={}\nstdout:\n{stdout}\nstderr:\n{stderr}",
        directory.display()
    );
    assert!(!timed_out, "bounded facade child timed out: {summary}");
    assert!(
        process_error.is_none(),
        "bounded facade observation failed: {summary}"
    );
    assert!(status.success(), "bounded facade child failed: {summary}");
    assert!(stdout.contains("phase=render status=done"));
    assert!(stdout.contains("phase=drop status=done"));
    assert!(stdout.contains("phase=after-small-operation status=done"));
}

#[test]
fn bounded_facade_lifecycle_child() {
    let Ok(case) = std::env::var(CASE_ENV) else {
        return;
    };
    std::thread::Builder::new()
        .name(format!("bounded-facade-{case}"))
        .stack_size(WORKER_STACK_BYTES)
        .spawn(move || run_worker(&case))
        .expect("spawn 2 MiB bounded facade worker")
        .join()
        .expect("bounded facade worker completes");
}

#[test]
fn railroad_boundary_native_svg_lifecycle_on_host_stack() {
    for case in ["railroad", "railroadEbnf", "railroadAbnf", "railroadPeg"] {
        run_child(case);
    }
}

#[test]
fn zenuml_boundary_native_svg_lifecycle_on_host_stack() {
    run_child("zenuml");
}

#[test]
fn tree_view_boundary_native_svg_lifecycle_on_host_stack() {
    run_child("treeView");
}

#[test]
fn usecase_json_boundary_native_svg_lifecycle_on_host_stack() {
    run_child("usecase");
}