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_html_payload::HtmlExportPayloadFactory;
use crate::export_inline_payload::InlineHtmlWriter;
use crate::forge::BuildGraph;
use crate::theme::KdvThemeSnapshot;
use katana_markdown_model::ListItemNode;

pub(crate) struct ListHtmlWriter;

impl ListHtmlWriter {
    pub(crate) fn append(
        html: &mut String,
        graph: &BuildGraph,
        theme: &KdvThemeSnapshot,
        ordered: bool,
        items: &[ListItemNode],
        fallback_text: &str,
    ) {
        let tag = if ordered { "ol" } else { "ul" };
        Self::append_list_open(html, tag, ordered, items);
        if items.is_empty() {
            html.push_str(&format!(
                "<li>{}</li>",
                ExportHtmlOps::render_text(fallback_text)
            ));
        } else {
            for item in items {
                Self::append_item(html, graph, theme, item);
            }
        }
        html.push_str(&format!("</{tag}>\n"));
    }

    fn append_list_open(html: &mut String, tag: &str, ordered: bool, items: &[ListItemNode]) {
        let start_attr = if ordered {
            Self::ordered_start_attr(items)
        } else {
            String::new()
        };
        html.push_str(&format!("<{tag}{start_attr}>"));
    }

    fn ordered_start_attr(items: &[ListItemNode]) -> String {
        items
            .first()
            .and_then(|item| item.ordered_number)
            .filter(|number| *number != 1)
            .map_or_else(String::new, |number| format!(" start=\"{number}\""))
    }

    fn append_item(
        html: &mut String,
        graph: &BuildGraph,
        theme: &KdvThemeSnapshot,
        item: &ListItemNode,
    ) {
        if let Some(marker) = &item.task_marker {
            html.push_str("<li data-kdv-task-item=\"true\">");
            Self::append_task_marker(html, marker);
        } else {
            html.push_str("<li>");
        }
        for node in &item.body {
            InlineHtmlWriter::append_node(html, node, theme);
        }
        for child in &item.children {
            HtmlExportPayloadFactory::append_node(html, graph, theme, child);
        }
        html.push_str("</li>");
    }

    fn append_task_marker(html: &mut String, marker: &str) {
        let state = Self::task_state(marker);
        let checked_attr = if state.checked { " checked" } else { "" };
        let mixed_attr = if state.mixed {
            " aria-checked=\"mixed\""
        } else {
            ""
        };
        html.push_str(&format!(
            "<input type=\"checkbox\" disabled data-kdv-task-marker=\"{}\" data-kdv-task-state=\"{}\"{mixed_attr}{checked_attr}>",
            ExportHtmlOps::escape_html(marker),
            state.label
        ));
        html.push_str(&format!(
            "<span data-kdv-task-visual=\"{}\" aria-hidden=\"true\"></span> ",
            state.visual_kind
        ));
    }

    fn task_state(marker: &str) -> TaskMarkerState {
        match marker {
            "[x]" => TaskMarkerState {
                label: "done",
                checked: true,
                mixed: false,
                visual_kind: "done-check",
            },
            "[-]" => TaskMarkerState {
                label: "in-progress",
                checked: false,
                mixed: true,
                visual_kind: "in-progress-dash",
            },
            "[/]" => TaskMarkerState {
                label: "in-progress",
                checked: false,
                mixed: true,
                visual_kind: "in-progress-slash",
            },
            _ => TaskMarkerState {
                label: "todo",
                checked: false,
                mixed: false,
                visual_kind: "todo",
            },
        }
    }
}

struct TaskMarkerState {
    label: &'static str,
    checked: bool,
    mixed: bool,
    visual_kind: &'static str,
}

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