katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use crate::export_html_ops::ExportHtmlOps;
use crate::export_math_payload::MathHtmlWriter;
use crate::export_semantics::EvaluatedMarkdownFragment;
use crate::html_sanitizer::HtmlFragmentNormalizer;
use crate::theme::KdvThemeSnapshot;
use katana_markdown_model::{KmmNode, KmmNodeKind};

pub(crate) struct InlineHtmlWriter;

impl InlineHtmlWriter {
    pub(crate) fn append_children(html: &mut String, node: &KmmNode, theme: &KdvThemeSnapshot) {
        for child in &node.children {
            Self::append_node(html, child, theme);
        }
    }

    pub(crate) fn append_fragment(html: &mut String, markdown: &str, theme: &KdvThemeSnapshot) {
        let fragment = EvaluatedMarkdownFragment::evaluate("table-cell.md", markdown);
        if !fragment.has_nodes() {
            html.push_str(&ExportHtmlOps::escape_html(markdown));
            return;
        }
        for node in fragment.nodes() {
            Self::append_fragment_node(html, node, theme);
        }
    }

    pub(crate) fn append_node(html: &mut String, node: &KmmNode, theme: &KdvThemeSnapshot) {
        match &node.kind {
            KmmNodeKind::Text(text) => Self::append_text(html, &text.text, theme),
            KmmNodeKind::Strong(span) => Self::append_span(html, node, "strong", &span.text, theme),
            KmmNodeKind::Emphasis(span) => Self::append_span(html, node, "em", &span.text, theme),
            KmmNodeKind::Strikethrough(span) => {
                Self::append_span(html, node, "s", &span.text, theme)
            }
            KmmNodeKind::InlineCode(code) => Self::append_tag(html, "code", &code.code),
            KmmNodeKind::InlineHtml(inline) => {
                html.push_str(&HtmlFragmentNormalizer::normalize(&inline.html))
            }
            KmmNodeKind::Link(link) => Self::append_link(html, link),
            KmmNodeKind::Image(image) => Self::append_image(html, image),
            KmmNodeKind::FootnoteReference(reference) => {
                Self::append_footnote_reference(html, &reference.label)
            }
            KmmNodeKind::InlineMath(math) => {
                Self::append_inline_math(html, &math.expression, theme)
            }
            KmmNodeKind::Emoji(emoji) => html.push_str(&ExportHtmlOps::escape_html(&emoji.value)),
            _ => html.push_str(&ExportHtmlOps::escape_html(&node.source.raw.text)),
        }
    }

    pub(crate) fn append_text(html: &mut String, text: &str, theme: &KdvThemeSnapshot) {
        let fragment = EvaluatedMarkdownFragment::evaluate("inline-text.md", text);
        if !fragment.contains_inline_markdown() {
            html.push_str(&ExportHtmlOps::render_text(text));
            return;
        }
        if !Self::try_append_inline_text(html, &fragment, theme) {
            html.push_str(&ExportHtmlOps::render_text(text));
        }
    }

    pub(crate) fn append_footnote_definition(
        html: &mut String,
        node: &KmmNode,
        label: &str,
        text: &str,
        theme: &KdvThemeSnapshot,
    ) {
        html.push_str(&format!(
            "<section id=\"fn-{}\" data-kdv-footnote-definition=\"{}\">",
            ExportHtmlOps::escape_html(label),
            ExportHtmlOps::escape_html(label)
        ));
        if node.children.is_empty() {
            html.push_str(&ExportHtmlOps::escape_html(text));
        } else {
            Self::append_children(html, node, theme);
        }
        html.push_str(&format!(
            " <a href=\"#fnref-{0}\" data-kdv-footnote-backref=\"{0}\">↩</a></section>\n",
            ExportHtmlOps::escape_html(label)
        ));
    }

    pub(crate) fn append_dollar_math_block(
        html: &mut String,
        expression: &str,
        theme: &KdvThemeSnapshot,
    ) {
        MathHtmlWriter::append_block(html, "dollar-block", expression, theme);
    }

    fn append_tag(html: &mut String, tag: &str, text: &str) {
        html.push_str(&format!(
            "<{tag}>{}</{tag}>",
            ExportHtmlOps::escape_html(text)
        ));
    }

    fn append_fragment_node(html: &mut String, node: &KmmNode, theme: &KdvThemeSnapshot) {
        match &node.kind {
            KmmNodeKind::Paragraph => {
                if node.children.is_empty() {
                    html.push_str(&ExportHtmlOps::render_text(&node.source.raw.text));
                } else {
                    Self::append_children(html, node, theme);
                }
            }
            _ => Self::append_node(html, node, theme),
        }
    }

    fn try_append_inline_text(
        html: &mut String,
        fragment: &EvaluatedMarkdownFragment,
        theme: &KdvThemeSnapshot,
    ) -> bool {
        if !fragment.has_nodes() || !fragment.contains_structured_inline() {
            return false;
        }
        for node in fragment.nodes() {
            Self::append_fragment_node(html, node, theme);
        }
        true
    }

    fn append_span(
        html: &mut String,
        node: &KmmNode,
        tag: &str,
        text: &str,
        theme: &KdvThemeSnapshot,
    ) {
        html.push_str(&format!("<{tag}>"));
        if node.children.is_empty() {
            html.push_str(&ExportHtmlOps::escape_html(text));
        } else {
            Self::append_children(html, node, theme);
        }
        html.push_str(&format!("</{tag}>"));
    }

    fn append_link(html: &mut String, link: &katana_markdown_model::LinkNode) {
        let title = link.title.as_ref().map_or_else(String::new, |value| {
            format!(" title=\"{}\"", ExportHtmlOps::escape_html(value))
        });
        let autolink = if link.autolink {
            " data-kdv-autolink=\"true\""
        } else {
            ""
        };
        html.push_str(&format!(
            "<a href=\"{}\"{title}{autolink}>{}</a>",
            ExportHtmlOps::escape_html(&link.destination),
            ExportHtmlOps::render_text(&link.label)
        ));
    }

    fn append_image(html: &mut String, image: &katana_markdown_model::ImageNode) {
        let title = image.title.as_ref().map_or_else(String::new, |value| {
            format!(" title=\"{}\"", ExportHtmlOps::escape_html(value))
        });
        html.push_str(&format!(
            "<img src=\"{}\" alt=\"{}\"{title}>",
            ExportHtmlOps::escape_html(&image.src),
            ExportHtmlOps::render_text(&image.alt)
        ));
    }

    fn append_footnote_reference(html: &mut String, label: &str) {
        let escaped_label = ExportHtmlOps::escape_html(label);
        html.push_str(&format!(
            "<sup id=\"fnref-{escaped_label}\" data-kdv-footnote-ref=\"{escaped_label}\"><a href=\"#fn-{escaped_label}\">[{escaped_label}]</a></sup>"
        ));
    }

    fn append_inline_math(html: &mut String, expression: &str, theme: &KdvThemeSnapshot) {
        MathHtmlWriter::append_inline(html, expression, theme);
    }
}

#[cfg(test)]
#[path = "export_inline_payload_tests.rs"]
mod tests;