katana-document-viewer 0.1.0

UI-independent document artifact, render evaluation, and export foundation for KatanA.
Documentation
use crate::export_html_ops::ExportHtmlOps;
use crate::export_html_payload::HtmlExportPayloadFactory;
use crate::export_semantics::EvaluatedMarkdownFragment;
use crate::forge::BuildGraph;
use crate::theme::KdvThemeSnapshot;

pub(crate) struct DetailsHtmlWriter;

impl DetailsHtmlWriter {
    pub(crate) fn try_append(
        html: &mut String,
        graph: &BuildGraph,
        theme: &KdvThemeSnapshot,
        fragment: &str,
    ) -> bool {
        let Some(parts) = DetailsParts::parse(fragment) else {
            return false;
        };
        html.push_str("<details data-kdv-accordion=\"true\" open><summary>");
        html.push_str(&ExportHtmlOps::escape_html(parts.summary.trim()));
        html.push_str("</summary><div data-kdv-accordion-body>\n");
        Self::append_markdown_body(html, graph, theme, parts.body);
        html.push_str("</div></details>");
        true
    }

    fn append_markdown_body(
        html: &mut String,
        graph: &BuildGraph,
        theme: &KdvThemeSnapshot,
        body: &str,
    ) {
        let fragment = EvaluatedMarkdownFragment::evaluate("details-body.md", body.trim());
        if !fragment.has_nodes() {
            html.push_str(&ExportHtmlOps::escape_html(body));
            return;
        }
        for node in fragment.nodes() {
            HtmlExportPayloadFactory::append_node(html, graph, theme, node);
        }
    }
}

struct DetailsParts<'a> {
    summary: &'a str,
    body: &'a str,
}

impl<'a> DetailsParts<'a> {
    fn parse(fragment: &'a str) -> Option<Self> {
        let trimmed = fragment.trim();
        if !trimmed.starts_with("<details") {
            return None;
        }
        let summary_start = trimmed.find("<summary>")? + "<summary>".len();
        let summary_end = trimmed.find("</summary>")?;
        let body_start = summary_end + "</summary>".len();
        let body_end = trimmed.rfind("</details>")?;
        let body = Self::strip_div(&trimmed[body_start..body_end]);
        Some(Self {
            summary: &trimmed[summary_start..summary_end],
            body,
        })
    }

    fn strip_div(value: &'a str) -> &'a str {
        let trimmed = value.trim();
        if let Some(body) = trimmed.strip_prefix("<div>") {
            return body.strip_suffix("</div>").unwrap_or(body);
        }
        trimmed
    }
}