katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use super::contract_test_support::HtmlContractTestSupport;
use crate::{
    BuildProfile, BuildRequest, CliApi, CliExportRequest, CliOutput, CliRequest, CliThemeMode,
    DocumentSnapshotFactory, DocumentSource, ExportFormat, KdvThemeSnapshot, ManifestOnlyBackend,
    RenderedDiagram, SourceKind, SourceRevision, SourceUri,
};
use katana_markdown_model::{KatanaMarkdownModel, MarkdownInput};

#[test]
fn app_export_requires_complete_theme_snapshot() -> Result<(), Box<dyn std::error::Error>> {
    let graph = build_graph("# Theme\n", KdvThemeSnapshot::katana_light())?;
    let html =
        HtmlContractTestSupport::export_html_with_graph(graph, KdvThemeSnapshot::katana_light())?;

    assert!(html.contains(r#"<html lang="ja" data-kdv-theme="katana-light">"#));
    assert!(html.contains("--kdv-text:#24292f;"));
    assert!(html.contains("--kdv-background:#ffffff;"));
    Ok(())
}

#[test]
fn cli_export_uses_katana_light_when_theme_mode_is_omitted()
-> Result<(), Box<dyn std::error::Error>> {
    let api = CliApi::new(ManifestOnlyBackend);
    let graph = build_graph("# Theme\n", KdvThemeSnapshot::katana_light())?;
    let output = api.handle(CliRequest::Export(CliExportRequest {
        graph,
        format: ExportFormat::Html,
        theme_mode: None,
    }))?;
    let CliOutput::Export { output, .. } = output else {
        return Err("expected export output".into());
    };
    let html = String::from_utf8(output.artifact.bytes.bytes)?;

    assert!(html.contains(r#"data-kdv-theme="katana-light""#));
    Ok(())
}

#[test]
fn cli_export_uses_katana_dark_when_dark_mode_is_selected() -> Result<(), Box<dyn std::error::Error>>
{
    let api = CliApi::new(ManifestOnlyBackend);
    let graph = build_graph("# Theme\n", KdvThemeSnapshot::katana_dark())?;
    let output = api.handle(CliRequest::Export(CliExportRequest {
        graph,
        format: ExportFormat::Html,
        theme_mode: Some(CliThemeMode::Dark),
    }))?;
    let CliOutput::Export { output, .. } = output else {
        return Err("expected export output".into());
    };
    let html = String::from_utf8(output.artifact.bytes.bytes)?;

    assert!(html.contains(r#"data-kdv-theme="katana-dark""#));
    assert!(html.contains("--kdv-background:#0d1117;"));
    Ok(())
}

#[test]
fn complete_theme_json_is_reflected_in_html_export() -> Result<(), Box<dyn std::error::Error>> {
    let mut theme = KdvThemeSnapshot::katana_dark();
    theme.name = "cli-json".to_string();
    theme.background = "#010203".to_string();
    let theme_json = serde_json::to_string(&theme)?;
    let cli_theme = serde_json::from_str::<KdvThemeSnapshot>(&theme_json)?;
    let graph = build_graph("# Theme\n", cli_theme.clone())?;

    let html = HtmlContractTestSupport::export_html_with_graph(graph, cli_theme)?;

    assert!(html.contains(r#"data-kdv-theme="cli-json""#));
    assert!(html.contains("--kdv-background:#010203;"));
    Ok(())
}

#[test]
fn app_dark_theme_marks_rendered_diagram_as_dark() -> Result<(), Box<dyn std::error::Error>> {
    let mut graph = build_graph(
        "```mermaid\ngraph TD; A-->B\n```\n",
        KdvThemeSnapshot::katana_dark(),
    )?;
    let node_id = graph.snapshot.document.nodes[0].id.0.clone();
    graph = graph.with_rendered_diagrams(vec![RenderedDiagram {
        node_id,
        kind: "mermaid".to_string(),
        svg: r#"<svg data-test="diagram"></svg>"#.to_string(),
    }]);

    let html =
        HtmlContractTestSupport::export_html_with_graph(graph, KdvThemeSnapshot::katana_dark())?;

    assert!(html.contains(r#"data-kdv-diagram-theme="dark""#));
    Ok(())
}

#[test]
fn diagram_background_is_transparent_not_code_block_background()
-> Result<(), Box<dyn std::error::Error>> {
    let theme = KdvThemeSnapshot::katana_light();
    let mut graph = build_graph(
        "```mermaid\ngraph TD; A-->B\n```\n",
        KdvThemeSnapshot::katana_light(),
    )?;
    let node_id = graph.snapshot.document.nodes[0].id.0.clone();
    graph = graph.with_rendered_diagrams(vec![RenderedDiagram {
        node_id,
        kind: "mermaid".to_string(),
        svg: r#"<svg data-test="diagram"></svg>"#.to_string(),
    }]);

    let html = HtmlContractTestSupport::export_html_with_graph(graph, theme.clone())?;
    let diagram_theme = theme.krr_theme();

    assert_eq!(theme.diagram_background, "transparent");
    assert_ne!(theme.diagram_background, theme.code_background);
    assert_eq!(diagram_theme.background, "transparent");
    assert!(html.contains("--kdv-diagram-background:transparent;"));
    assert!(html.contains("figure[data-kdv-diagram]{background:var(--kdv-diagram-background);"));
    assert!(!html.contains("figure[data-kdv-diagram]{background:var(--kdv-code-bg)"));
    Ok(())
}

fn build_graph(
    markdown: &str,
    theme: KdvThemeSnapshot,
) -> Result<crate::BuildGraph, Box<dyn std::error::Error>> {
    let source = DocumentSource {
        uri: SourceUri("file:///theme.md".to_string()),
        kind: SourceKind::Markdown,
        revision: SourceRevision("theme".to_string()),
        content: markdown.to_string(),
    };
    let document = KatanaMarkdownModel::parse(MarkdownInput::from_content(
        "theme.md",
        markdown.to_string(),
    ))?;
    let snapshot = DocumentSnapshotFactory::from_kmm(source, document);
    Ok(crate::BuildGraph::from_request(&BuildRequest {
        snapshot,
        profile: BuildProfile::markdown_export(),
        theme,
    }))
}