katana-document-viewer 0.1.0

UI-independent document artifact, render evaluation, and export foundation for KatanA.
Documentation
use katana_markdown_model::{KmmDocument, KmmError, KmmNode, KmmNodeId, KmmNodeKind, SourceSpan};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DocumentId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceUri(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceRevision(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SourceKind {
    Markdown,
    Image,
    Pdf,
    Binary,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocumentKind {
    Markdown,
    Image,
    Pdf,
    Office,
    Unknown,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentSource {
    pub uri: SourceUri,
    pub kind: SourceKind,
    pub revision: SourceRevision,
    pub content: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentMetadataView {
    pub unresolved_count: usize,
    pub diagnostic_keys: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentOutline {
    pub items: Vec<DocumentOutlineItem>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentOutlineItem {
    pub node_id: KmmNodeId,
    pub level: u8,
    pub text: String,
    pub source: SourceSpan,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentSnapshot {
    pub id: DocumentId,
    pub kind: DocumentKind,
    pub source_uri: SourceUri,
    pub revision: SourceRevision,
    pub source_path: PathBuf,
    pub document: KmmDocument,
    pub outline: DocumentOutline,
    pub metadata: DocumentMetadataView,
}

#[derive(Debug, Error)]
pub enum DocumentModelError {
    #[error("KMM parse failed: {0}")]
    KmmParse(#[from] KmmError),
}

pub struct DocumentSnapshotFactory;

impl DocumentSnapshotFactory {
    pub fn from_kmm(source: DocumentSource, document: KmmDocument) -> DocumentSnapshot {
        let outline = DocumentOutlineBuilder::build(&document);
        DocumentSnapshot {
            id: DocumentId(document.fingerprint.value.clone()),
            kind: DocumentKind::Markdown,
            source_uri: source.uri,
            revision: source.revision,
            source_path: document.path.clone(),
            document,
            outline,
            metadata: DocumentMetadataView {
                unresolved_count: 0,
                diagnostic_keys: Vec::new(),
            },
        }
    }

    pub fn from_parse_result(
        source: DocumentSource,
        result: Result<KmmDocument, KmmError>,
    ) -> Result<DocumentSnapshot, DocumentModelError> {
        Ok(Self::from_kmm(source, result?))
    }
}

struct DocumentOutlineBuilder;

impl DocumentOutlineBuilder {
    fn build(document: &KmmDocument) -> DocumentOutline {
        let mut items = Vec::new();
        for node in &document.nodes {
            Self::collect_node(node, &mut items);
        }
        DocumentOutline { items }
    }

    fn collect_node(node: &KmmNode, items: &mut Vec<DocumentOutlineItem>) {
        if let KmmNodeKind::Heading(heading) = &node.kind {
            items.push(DocumentOutlineItem {
                node_id: node.id.clone(),
                level: heading.level,
                text: heading.text.clone(),
                source: node.source.clone(),
            });
        }
        for child in &node.children {
            Self::collect_node(child, items);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use katana_markdown_model::{
        ByteRange, HeadingNode, LineColumn, LineColumnRange, RawSnippet, TextFingerprint,
    };

    #[test]
    fn snapshot_keeps_kmm_outline_and_revision() {
        let source = DocumentSource {
            uri: SourceUri("file:///sample.md".to_string()),
            kind: SourceKind::Markdown,
            revision: SourceRevision("rev-1".to_string()),
            content: "# Title".to_string(),
        };
        let snapshot = DocumentSnapshotFactory::from_kmm(source, sample_document());

        assert_eq!(snapshot.revision, SourceRevision("rev-1".to_string()));
        assert_eq!(snapshot.outline.items.len(), 1);
        assert_eq!(snapshot.outline.items[0].text, "Title");
    }

    fn sample_document() -> KmmDocument {
        KmmDocument {
            path: PathBuf::from("sample.md"),
            fingerprint: TextFingerprint {
                algorithm: "test".to_string(),
                value: "doc-1".to_string(),
            },
            nodes: vec![KmmNode {
                id: KmmNodeId("node-1".to_string()),
                kind: KmmNodeKind::Heading(HeadingNode {
                    level: 1,
                    text: "Title".to_string(),
                }),
                source: sample_span("# Title"),
                children: Vec::new(),
            }],
        }
    }

    fn sample_span(text: &str) -> SourceSpan {
        SourceSpan {
            byte_range: ByteRange {
                start: 0,
                end: text.len(),
            },
            line_column_range: LineColumnRange {
                start: LineColumn { line: 1, column: 1 },
                end: LineColumn {
                    line: 1,
                    column: text.len() + 1,
                },
            },
            raw: RawSnippet {
                text: text.to_string(),
            },
        }
    }
}