katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use super::contract_test_support::HtmlContractTestSupport;

#[test]
fn red_detects_math_html_contract_gaps() -> Result<(), Box<dyn std::error::Error>> {
    let html = HtmlContractTestSupport::export_html(math_contract_markdown())?;
    assert_math_contract_contains(&html);
    assert_math_contract_hides_raw_markdown(&html);
    Ok(())
}

fn math_contract_markdown() -> &'static str {
    r##"```math
f(x) = x^2
```

質量: $E=mc^2$

$$ \sum_{k=1}^{n} k = \frac{n(n+1)}{2} $$
"##
}

fn assert_math_contract_contains(html: &str) {
    HtmlContractTestSupport::assert_contains_all(
        html,
        &[
            (
                "fenced math block",
                r#"<div data-kdv-math="block" data-kdv-render-runtime="katana-render-runtime">"#,
            ),
            (
                "compact inline math",
                r#"<span data-kdv-math="inline" data-kdv-render-runtime="katana-render-runtime">"#,
            ),
            (
                "one line dollar math",
                r#"<div data-kdv-math="dollar-block" data-kdv-render-runtime="katana-render-runtime">"#,
            ),
            ("svg output", "<svg"),
        ],
    );
}

fn assert_math_contract_hides_raw_markdown(html: &str) {
    HtmlContractTestSupport::assert_not_contains_any(
        html,
        &[
            ("math fence marker", "```math"),
            (
                "direct math renderer leak",
                "data-kdv-math-renderer=\"mathjax-svg\"",
            ),
        ],
    );
}

#[test]
fn red_detects_katana_specific_html_contract_gaps() -> Result<(), Box<dyn std::error::Error>> {
    let html = HtmlContractTestSupport::export_html(katana_specific_markdown())?;
    assert_katana_specific_contract(&html);
    Ok(())
}

fn katana_specific_markdown() -> &'static str {
    r##"- [-] 完了扱い
- [/] 進行中

柔軟な数式: $ E = mc^2 $ と $$ a+b=c $$
"##
}

fn assert_katana_specific_contract(html: &str) {
    HtmlContractTestSupport::assert_contains_all(
        html,
        &[
            (
                "task done marker",
                r#"<input type="checkbox" disabled data-kdv-task-marker="[-]" data-kdv-task-state="in-progress" aria-checked="mixed">"#,
            ),
            (
                "task in progress marker",
                r#"<input type="checkbox" disabled data-kdv-task-marker="[/]" data-kdv-task-state="in-progress" aria-checked="mixed">"#,
            ),
            (
                "spaced inline math",
                r#"<span data-kdv-math="inline" data-kdv-render-runtime="katana-render-runtime">"#,
            ),
            (
                "inline dollar block",
                r#"<span data-kdv-math="inline" data-kdv-render-runtime="katana-render-runtime">"#,
            ),
            ("svg output", "<svg"),
        ],
    );
}

#[test]
fn red_detects_math_svg_color_in_html_contract() -> Result<(), Box<dyn std::error::Error>> {
    let html = HtmlContractTestSupport::export_html(
        r##"本文: $E=mc^2$ の式と

```math
x^2 + y^2 = r^2
```

$$ \frac{1}{2} = 0.5 $$

$A+B=C$
"##,
    )?;

    let inline_svg = extract_math_svg(&html, "span", "inline").ok_or_else(|| {
        std::io::Error::other(format!("inline math should contain an svg element: {html}"))
    })?;
    assert_math_svg_uses_color(&inline_svg, "#24292f");
    let block_svg = extract_math_svg(&html, "div", "block").ok_or_else(|| {
        std::io::Error::other(format!("math block should contain an svg element: {html}"))
    })?;
    assert_math_svg_uses_color(&block_svg, "#24292f");
    let one_line_svg = extract_math_svg(&html, "div", "dollar-block").ok_or_else(|| {
        std::io::Error::other(format!(
            "one line dollar math should contain an svg element: {html}"
        ))
    })?;
    assert_math_svg_uses_color(&one_line_svg, "#24292f");
    Ok(())
}

#[test]
fn app_supplied_complete_theme_reaches_krr_math_svg() -> Result<(), Box<dyn std::error::Error>> {
    let mut theme = crate::KdvThemeSnapshot::katana_light();
    theme.name = "app-supplied-light".to_string();
    theme.text = "#123456".to_string();

    let html = HtmlContractTestSupport::export_html_with_theme("本文: $E=mc^2$\n", theme)?;
    let inline_svg = extract_math_svg(&html, "span", "inline")
        .ok_or("inline math should contain an svg element")?;

    assert_math_svg_uses_color(&inline_svg, "#123456");
    assert!(!inline_svg.to_ascii_lowercase().contains("#24292f"));
    Ok(())
}

#[test]
fn inline_math_svg_must_render_full_expression() -> Result<(), Box<dyn std::error::Error>> {
    let html = HtmlContractTestSupport::export_html("質量とエネルギーの等価原理: $E = mc^2$\n")?;
    let inline_svg = extract_math_svg(&html, "span", "inline")
        .ok_or("inline math should contain an svg element")?;

    assert!(
        inline_svg.contains(r#"data-latex="E = mc^2""#),
        "inline math root metadata must keep the full expression: {inline_svg}"
    );
    assert_eq!(
        extract_data_latex(&inline_svg).as_deref(),
        Some("E = mc^2"),
        "inline math svg should carry the full raw expression as data-latex: {inline_svg}"
    );
    assert!(
        inline_svg.contains(r#"data-latex="=""#)
            && inline_svg.contains(r#"data-latex="m""#)
            && inline_svg.contains(r#"data-latex="c""#),
        "inline math must render the full expression, not only the first token: {inline_svg}"
    );
    Ok(())
}

fn extract_math_svg(html: &str, tag: &str, role: &str) -> Option<String> {
    let marker = format!(r#"<{tag} data-kdv-math="{role}""#);
    let marker_index = html.find(&marker)?;
    let remainder = &html[marker_index..];
    let svg_start = remainder.find("<svg")?;
    let svg_end = remainder[svg_start..].find("</svg>")?;
    Some(remainder[svg_start..svg_start + svg_end + "</svg>".len()].to_string())
}

fn extract_data_latex(svg: &str) -> Option<String> {
    let marker = "data-latex=\"";
    let start = svg.find(marker)?;
    let value_start = start + marker.len();
    let value_end = svg[value_start..].find('\"')? + value_start;
    Some(svg[value_start..value_end].to_string())
}

fn assert_math_svg_uses_color(svg: &str, expected: &str) {
    let normalized = svg.to_ascii_lowercase().replace(' ', "");
    let expected = expected.to_ascii_lowercase();
    assert!(
        normalized.contains(&format!("color:{expected}")) || normalized.contains(&expected),
        "math svg should use theme text color {expected}: {svg}"
    );
    assert!(
        !normalized.contains("#e0e0e0"),
        "math svg should not fall back to KRR dark default: {svg}"
    );
}