katana-document-viewer 0.1.0

UI-independent document artifact, render evaluation, and export foundation for KatanA.
Documentation
use super::*;
use crate::{
    BuildProfile, DocumentSnapshotFactory, DocumentSource, ForgePipeline, KdvThemeSnapshot,
    SourceKind, SourceRevision, SourceUri,
};
use katana_markdown_model::{KatanaMarkdownModel, MarkdownInput};
use katana_render_runtime::{RenderThemeMode, RenderThemeSnapshot};
use std::sync::{Arc, Mutex};

#[test]
fn diagram_renderer_panic_is_recorded_as_diagnostic() -> Result<(), Box<dyn std::error::Error>> {
    let pipeline = ForgePipeline::new(DiagramRenderingBackend::new(PanicDiagramEngine));
    let source = DocumentSource {
        uri: SourceUri("file:///panic.md".to_string()),
        kind: SourceKind::Markdown,
        revision: SourceRevision("rev-1".to_string()),
        content: "```mermaid\ngraph TD\n  A --> B\n```".to_string(),
    };
    let document = KatanaMarkdownModel::parse(MarkdownInput::from_content(
        "panic.md",
        source.content.clone(),
    ))?;
    let snapshot = DocumentSnapshotFactory::from_kmm(source, document);
    let graph = pipeline.build(&BuildRequest {
        snapshot,
        profile: BuildProfile::markdown_export(),
        theme: KdvThemeSnapshot::katana_light(),
    })?;

    assert!(graph.rendered_diagrams.is_empty());
    assert!(
        graph
            .diagnostics
            .messages
            .iter()
            .any(|message| { message.contains("diagram renderer panicked") })
    );
    Ok(())
}

#[test]
fn diagram_renderer_receives_app_supplied_complete_theme() -> Result<(), Box<dyn std::error::Error>>
{
    let captured_themes = Arc::new(Mutex::new(Vec::new()));
    let pipeline = ForgePipeline::new(DiagramRenderingBackend::new(RecordingDiagramEngine {
        themes: captured_themes.clone(),
    }));
    let graph = pipeline.build(&BuildRequest {
        snapshot: snapshot_from_markdown("```plantuml\n@startuml\nAlice -> Bob\n@enduml\n```")?,
        profile: BuildProfile::markdown_export(),
        theme: app_supplied_dark_theme(),
    })?;

    assert_eq!(graph.rendered_diagrams.len(), 1);
    let themes = captured_themes
        .lock()
        .map_err(|error| format!("theme capture lock failed: {error}"))?;
    let captured = themes.first().ok_or("captured KRR theme is missing")?;
    assert_app_supplied_theme_forwarded(captured);
    Ok(())
}

fn app_supplied_dark_theme() -> KdvThemeSnapshot {
    let mut theme = KdvThemeSnapshot::katana_dark();
    theme.name = "app-supplied-dark".to_string();
    theme.diagram_background = "transparent".to_string();
    theme.diagram_text = "#abcdef".to_string();
    theme.diagram_fill = "#123456".to_string();
    theme.diagram_stroke = "#654321".to_string();
    theme.diagram_arrow = "#fedcba".to_string();
    theme.mermaid_theme = "dark".to_string();
    theme
}

fn assert_app_supplied_theme_forwarded(captured: &RenderThemeSnapshot) {
    assert_eq!(captured.mode, RenderThemeMode::Dark);
    assert_eq!(captured.background, "transparent");
    assert_eq!(captured.text, "#abcdef");
    assert_eq!(captured.fill, "#123456");
    assert_eq!(captured.stroke, "#654321");
    assert_eq!(captured.arrow, "#fedcba");
    assert_eq!(captured.mermaid_theme, "dark");
}

struct PanicDiagramEngine;

impl DiagramRenderEngine for PanicDiagramEngine {
    fn render(&self, _request: DiagramRenderRequest<'_>) -> Result<RenderedDiagram, String> {
        std::panic::resume_unwind(Box::new("diagram backend panic"));
    }
}

struct RecordingDiagramEngine {
    themes: Arc<Mutex<Vec<RenderThemeSnapshot>>>,
}

impl DiagramRenderEngine for RecordingDiagramEngine {
    fn render(&self, request: DiagramRenderRequest<'_>) -> Result<RenderedDiagram, String> {
        self.themes
            .lock()
            .map_err(|error| error.to_string())?
            .push(request.theme.krr_theme());
        Ok(RenderedDiagram {
            node_id: request.node_id.to_string(),
            kind: ExportHtmlOps::diagram_kind_label(&request.kind).to_string(),
            svg: "<svg data-test=\"theme-forwarded\"></svg>".to_string(),
        })
    }
}

fn snapshot_from_markdown(
    markdown: &str,
) -> Result<crate::DocumentSnapshot, Box<dyn std::error::Error>> {
    let source = DocumentSource {
        uri: SourceUri("file:///diagram-theme.md".to_string()),
        kind: SourceKind::Markdown,
        revision: SourceRevision("rev-1".to_string()),
        content: markdown.to_string(),
    };
    let document =
        KatanaMarkdownModel::parse(MarkdownInput::from_content("diagram-theme.md", markdown))?;
    Ok(DocumentSnapshotFactory::from_kmm(source, document))
}