katana-canvas-forge 0.1.7

Versioned diagram rendering and document export runtime for KatanA (Mermaid, Draw.io, HTML/PDF/PNG/JPEG).
Documentation
use crate::markdown::diagram_js_runtime::DiagramRuntimeScript;

pub(super) struct MermaidRuntimeScripts;

impl MermaidRuntimeScripts {
    pub(super) fn build<'a>(bundle: &'a str, request_json: &str) -> Vec<DiagramRuntimeScript<'a>> {
        Self::build_with_zenuml(bundle, MERMAID_ZENUML, request_json)
    }

    fn build_with_zenuml<'a>(
        bundle: &'a str,
        zenuml_bundle: &'a str,
        request_json: &str,
    ) -> Vec<DiagramRuntimeScript<'a>> {
        vec![
            DiagramRuntimeScript::borrowed("mermaid-runtime.min.js", MERMAID_RUNTIME),
            DiagramRuntimeScript::borrowed("mermaid.min.js", bundle),
            DiagramRuntimeScript::borrowed("mermaid-zenuml.min.js", zenuml_bundle),
            DiagramRuntimeScript::owned("render-mermaid.js", render_script(request_json)),
        ]
    }
}

fn render_script(request_json: &str) -> String {
    format!("katanaInstallMermaidZenumlRuntimeAdapter();\nkatanaRunMermaidRuntime({request_json});")
}

const MERMAID_RUNTIME: &str = include_str!("../diagram_runtime/generated/mermaid-runtime.min.js");
const MERMAID_ZENUML: &str =
    include_str!("../../../vendor/mermaid-zenuml/0.2.2/mermaid-zenuml.min.js");

#[cfg(test)]
mod tests {
    use super::MermaidRuntimeScripts;
    use crate::markdown::diagram_js_runtime::DiagramV8Runtime;

    #[test]
    fn build_includes_bundle_and_render_script() {
        let scripts = MermaidRuntimeScripts::build("bundle", "{}");
        assert!(scripts.iter().any(|it| it.name == "mermaid-runtime.min.js"));
        assert!(scripts.iter().any(|it| it.name == "mermaid.min.js"));
        assert!(scripts.iter().any(|it| it.name == "mermaid-zenuml.min.js"));
        assert!(scripts.iter().any(|it| it.name == "render-mermaid.js"));
    }

    #[test]
    fn zenuml_registration_runs_before_render() {
        let scripts = MermaidRuntimeScripts::build_with_zenuml(
            fake_mermaid(),
            fake_zenuml(),
            r##"{"source":"zenuml\nA.method()","svgId":"id","theme":"dark","background":"#000","fill":"#111","text":"#fff","stroke":"#fff","arrow":"#fff","diagramType":"zenuml"}"##,
        );

        let rendered = DiagramV8Runtime::render(&scripts);

        assert!(
            rendered.as_ref().is_ok_and(|it| it.contains("registered")),
            "{rendered:?}"
        );
    }

    #[test]
    fn zenuml_directive_source_registers_external_diagram_without_request_hint() {
        let scripts = MermaidRuntimeScripts::build_with_zenuml(
            fake_mermaid(),
            fake_zenuml(),
            r##"{"source":"%%{init: { \"theme\": \"dark\" }}%%\n%% comment\nzenuml\nA.method()","svgId":"id","theme":"dark","background":"#000","fill":"#111","text":"#fff","stroke":"#fff","arrow":"#fff"}"##,
        );

        let rendered = DiagramV8Runtime::render(&scripts);

        assert!(
            rendered.as_ref().is_ok_and(|it| it.contains("registered")),
            "{rendered:?}"
        );
    }

    fn fake_mermaid() -> &'static str {
        r#"
globalThis.mermaid = {
  initialize() {},
  registerExternalDiagrams: async (diagrams) => {
    globalThis.__registeredDiagram = diagrams[0].id;
  },
  render: async (id) => {
    const text = globalThis.__registeredDiagram ?? "missing";
    return { svg: `<svg id="${id}"><text>${text}</text></svg>` };
  }
};
"#
    }

    fn fake_zenuml() -> &'static str {
        r#"globalThis["mermaid-zenuml"] = { id: "registered" };"#
    }
}