drawlang-render 0.1.2

SVG/PNG/PDF backends for the drawlang DSL
Documentation
use drawlang_core::{compile, layout};
use drawlang_render::{render_png, render_svg};

fn svg_of(src: &str) -> String {
    let out = compile(src);
    assert!(
        out.diagnostics.is_empty(),
        "{:?}",
        out.diagnostics
            .iter()
            .map(|d| d.message.clone())
            .collect::<Vec<_>>()
    );
    let l = layout(&out.doc);
    render_svg(&out.doc, &l.geometry)
}

#[test]
fn shapes_render() {
    let svg = svg_of(
        "drawl 0.1\na { label: \"r\"; shape: rect }\nb { label: \"p\"; shape: pill }\nc { label: \"e\"; shape: ellipse }\n",
    );
    assert!(svg.contains("<ellipse"), "ellipse element");
    // Pill = rect whose rx is half its height.
    assert!(svg.matches("<rect").count() >= 3, "background + two rects");
}

#[test]
fn dark_theme_uses_dark_palette() {
    let dark = svg_of("drawl 0.1\ncanvas { theme: dark }\nn { label: \"x\" }\n");
    assert!(dark.contains("#15171C"), "dark background");
    let paper = svg_of("drawl 0.1\nn { label: \"x\" }\n");
    assert!(paper.contains("#FAF9F7"), "paper background");
}

#[test]
fn theme_tokens_resolve_per_theme() {
    let src_paper = "drawl 0.1\nn { label: \"x\"; color: @accent }\n";
    let src_dark = "drawl 0.1\ncanvas { theme: dark }\nn { label: \"x\"; color: @accent }\n";
    assert!(svg_of(src_paper).contains("#2F6FED"), "paper accent");
    assert!(svg_of(src_dark).contains("#5C8DEF"), "dark accent");
}

#[test]
fn auto_contrast_flips_text_on_dark_fill() {
    let svg = svg_of("drawl 0.1\nn { label: \"x\"; fill: @accent }\n");
    // Accent blue is dark; the label must switch to the light text color.
    assert!(
        svg.contains(r##"fill="#FAFBFC">x</text>"##),
        "light text on dark fill:\n{svg}"
    );
}

#[test]
fn dashed_edges_and_stroke_width() {
    let svg = svg_of("drawl 0.1\na\nb\na -> b { dashed: true; stroke: 3 }\n");
    assert!(svg.contains("stroke-dasharray"), "dashes");
    assert!(svg.contains(r#"stroke-width="3""#), "stroke width");
}

#[test]
fn bidirectional_edge_has_two_arrowheads() {
    let svg = svg_of("drawl 0.1\na { label: \"A\" }\nb { label: \"B\" }\na <-> b\n");
    // Arrowheads are filled triangle paths ending in `Z`.
    let arrows = svg.matches(r##"Z" fill="#"##).count();
    assert!(arrows >= 2, "expected 2 arrowheads, found {arrows}\n{svg}");
}

#[test]
fn png_renders_nonempty() {
    let svg = svg_of("drawl 0.1\na { label: \"A\" }\nb { label: \"B\" }\na -> b : \"edge\"\n");
    let png = render_png(&svg, 1.0).expect("png");
    assert!(png.len() > 1000, "plausible PNG size");
    assert_eq!(&png[1..4], b"PNG");
}

#[test]
fn group_label_and_wash_render() {
    let svg = svg_of("drawl 0.1\ngroup g \"Cluster\" { n { label: \"x\" } }\n");
    assert!(svg.contains(">Cluster</text>"));
    assert!(svg.contains("fill-opacity"), "group wash");
}