merman-cli 0.8.0-alpha.3

CLI to parse/layout/render Mermaid diagrams to SVG/PNG/JPG/PDF/ASCII/Unicode (headless).
mod executor;
mod icons;
mod markdown_export;
mod plan;
mod raster;
mod svg_pipeline;

pub(crate) use executor::run_render;
pub(crate) use plan::{render_plan_for_mmdc, render_plan_for_subcommand};

#[cfg(test)]
mod tests {
    use super::executor::RenderRequest;
    use super::plan::{RenderMode, RenderPlan};
    use super::raster::RasterCliOptions;
    use super::svg_pipeline::svg_postprocess_pipeline;
    use crate::cli::{
        ParseCliArgs, RenderCliArgs, RenderFormat, SvgPipelineKind, TextOutputCliArgs,
    };
    use crate::io::OutputTarget;
    use merman::render::SvgPipeline;
    use merman::{Engine, ParseOptions};

    fn test_plan(format: RenderFormat) -> RenderPlan {
        RenderPlan {
            input: None,
            output: None::<OutputTarget>,
            format,
            parse: ParseCliArgs::default(),
            render: RenderCliArgs::default(),
            scale: 1.0,
            raster: RasterCliOptions::default(),
            background: Some("#f8fafc".to_string()),
            css: Some(".node { fill: red; }".to_string()),
            svg_pipeline: None,
            icon_registry: None,
            artefacts: None,
            jobs: 1,
            pdf_fit: true,
            quiet: true,
            text: TextOutputCliArgs::default(),
            mode: RenderMode::Subcommand,
        }
    }

    #[test]
    fn diagram_raster_pipeline_uses_resvg_safe_before_cli_postprocessors() {
        let mut plan = test_plan(RenderFormat::Png);
        plan.svg_pipeline = Some(SvgPipelineKind::Readable);
        let engine = Engine::new();
        let request = RenderRequest {
            plan: &plan,
            engine: &engine,
            parse_options: ParseOptions::default(),
            math_renderer: None,
        };
        let svg = r#"<svg id="diagram" xmlns="http://www.w3.org/2000/svg"><style>@keyframes bad { to { opacity: .5; } } .node { animation: bad 1s; }</style><foreignObject width="40" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><p>Raw</p></div></foreignObject><rect class="node" width="10px" height="12px" stroke=""/></svg>"#;

        let out = request
            .postprocess_pipeline()
            .process_to_string(svg)
            .unwrap();

        assert!(!out.contains("<foreignObject"));
        assert!(!out.contains("@keyframes bad"));
        assert!(!out.contains("animation: bad"));
        assert!(out.contains(r#"style="background-color: #f8fafc;""#));
        assert_eq!(
            out.matches(r#"data-merman-postprocess="scoped-css""#)
                .count(),
            1
        );
    }

    #[test]
    fn diagram_svg_pipeline_keeps_parity_base_before_cli_postprocessors() {
        let plan = test_plan(RenderFormat::Svg);
        let engine = Engine::new();
        let request = RenderRequest {
            plan: &plan,
            engine: &engine,
            parse_options: ParseOptions::default(),
            math_renderer: None,
        };
        let svg = r#"<svg id="diagram" xmlns="http://www.w3.org/2000/svg"><foreignObject width="40" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><p>Raw</p></div></foreignObject><rect class="node" width="10px" height="12px" stroke=""/></svg>"#;

        let out = request
            .postprocess_pipeline()
            .process_to_string(svg)
            .unwrap();

        assert!(out.contains("<foreignObject"));
        assert!(out.contains(r#"style="background-color: #f8fafc;""#));
        assert_eq!(
            out.matches(r#"data-merman-postprocess="scoped-css""#)
                .count(),
            1
        );
    }

    #[test]
    fn diagram_svg_pipeline_can_request_resvg_safe_before_cli_postprocessors() {
        let mut plan = test_plan(RenderFormat::Svg);
        plan.svg_pipeline = Some(SvgPipelineKind::ResvgSafe);
        let engine = Engine::new();
        let request = RenderRequest {
            plan: &plan,
            engine: &engine,
            parse_options: ParseOptions::default(),
            math_renderer: None,
        };
        let svg = r#"<svg id="diagram" xmlns="http://www.w3.org/2000/svg"><foreignObject width="40" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><p>Raw</p></div></foreignObject><rect class="node" width="10px" height="12px" stroke=""/></svg>"#;

        let out = request
            .postprocess_pipeline()
            .process_to_string(svg)
            .unwrap();

        assert!(!out.contains("<foreignObject"));
        assert!(out.contains(r#"data-merman-foreignobject="fallback""#));
        assert!(out.contains(r#"style="background-color: #f8fafc;""#));
        assert_eq!(
            out.matches(r#"data-merman-postprocess="scoped-css""#)
                .count(),
            1
        );
    }

    #[test]
    fn raw_svg_raster_pipeline_sanitizes_before_cli_postprocessors() {
        let pipeline = svg_postprocess_pipeline(
            SvgPipeline::resvg_safe(),
            Some("#f8fafc"),
            Some(".node { fill: red; }"),
        );
        let svg = r#"<svg id="raw" xmlns="http://www.w3.org/2000/svg"><style>@keyframes bad { to { opacity: .5; } } .node { animation: bad 1s; }</style><foreignObject width="40" height="20"><div xmlns="http://www.w3.org/1999/xhtml"><p>Raw</p></div></foreignObject><rect class="node" width="10px" height="12px" stroke=""/></svg>"#;

        let out = pipeline.process_to_string(svg).unwrap();

        assert!(!out.contains("<foreignObject"));
        assert!(!out.contains("@keyframes bad"));
        assert!(!out.contains("animation: bad"));
        assert!(out.contains(r#"style="background-color: #f8fafc;""#));
        assert!(out.contains(r#"data-merman-postprocess="scoped-css""#));
        assert!(out.contains("#raw .node { fill: red; }"));
    }
}