sphinx-ultra 0.5.0

High-performance Rust-based Sphinx documentation builder for large codebases
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

// Custom serialization for PathBuf to handle cross-platform compatibility
fn serialize_pathbuf<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&path.to_string_lossy())
}

fn deserialize_pathbuf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    Ok(PathBuf::from(s))
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
    /// Source file path
    #[serde(
        serialize_with = "serialize_pathbuf",
        deserialize_with = "deserialize_pathbuf"
    )]
    pub source_path: PathBuf,

    /// Output file path
    #[serde(
        serialize_with = "serialize_pathbuf",
        deserialize_with = "deserialize_pathbuf"
    )]
    pub output_path: PathBuf,

    /// Document title
    pub title: String,

    /// Document content (parsed)
    pub content: DocumentContent,

    /// Document metadata
    pub metadata: DocumentMetadata,

    /// Rendered HTML content
    pub html: String,

    /// Source file modification time
    pub source_mtime: DateTime<Utc>,

    /// Build time
    pub build_time: DateTime<Utc>,

    /// Cross-references found in this document
    pub cross_refs: Vec<CrossReference>,

    /// Table of contents
    pub toc: Vec<TocEntry>,

    /// Toctree directives found in this document (wave 3: derived from
    /// the doctree at parse time; the build resolves/warns).
    pub toctrees: Vec<crate::rst::ToctreeRecord>,

    /// Directive occurrences for the validation system.
    pub directive_records: Vec<crate::rst::DirectiveRecord>,

    /// Role occurrences for validation + nitpicky cross-ref checking.
    pub role_records: Vec<crate::rst::RoleRecord>,

    /// Explicit hyperlink-target labels (docutils-normalized names).
    pub labels: Vec<LabelRecord>,

    /// The parse's id/name registry snapshot (docutils `document.nameids`),
    /// which the std-domain label harvest consumes. It rides the `Document`
    /// — rather than being a separate read-phase value — so an incremental
    /// cache hit, which skips parsing entirely, still has the real registry
    /// instead of an empty stand-in. Deliberately *not* `#[serde(default)]`:
    /// a cache entry written before this field existed fails to decode and
    /// is re-parsed, which is the honest outcome.
    pub registry: crate::rst::RegistryExport,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabelRecord {
    pub name: String,
    /// 1-based source line of the target marker.
    pub line: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DocumentContent {
    RestructuredText(RstContent),
    Markdown(MarkdownContent),
    PlainText(String),
}

impl std::fmt::Display for DocumentContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DocumentContent::RestructuredText(rst) => write!(f, "{}", rst.raw),
            DocumentContent::Markdown(md) => write!(f, "{}", md.raw),
            DocumentContent::PlainText(text) => write!(f, "{}", text),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RstContent {
    /// Raw RST content
    pub raw: String,

    /// Parsed AST
    pub ast: Vec<RstNode>,

    /// Directives found in the document
    pub directives: Vec<RstDirective>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarkdownContent {
    /// Raw Markdown content
    pub raw: String,

    /// Parsed AST
    pub ast: Vec<MarkdownNode>,

    /// Front matter
    pub front_matter: Option<serde_yaml::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DocumentMetadata {
    /// Document author(s)
    pub authors: Vec<String>,

    /// Document creation date
    pub created: Option<DateTime<Utc>>,

    /// Document last modified date
    pub modified: Option<DateTime<Utc>>,

    /// Document tags
    pub tags: Vec<String>,

    /// Document category
    pub category: Option<String>,

    /// Custom metadata fields
    pub custom: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossReference {
    /// Reference type (doc, ref, func, class, etc.)
    pub ref_type: String,

    /// Reference target
    pub target: String,

    /// Reference text
    pub text: Option<String>,

    /// Line number where reference appears
    pub line_number: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocEntry {
    /// Entry title
    pub title: String,

    /// Entry level (1-6)
    pub level: usize,

    /// Anchor ID
    pub anchor: String,

    /// Line number
    pub line_number: usize,

    /// Child entries
    pub children: Vec<TocEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RstNode {
    Title {
        text: String,
        level: usize,
        line: usize,
    },
    Paragraph {
        content: String,
        line: usize,
    },
    CodeBlock {
        language: Option<String>,
        content: String,
        line: usize,
    },
    List {
        items: Vec<String>,
        ordered: bool,
        line: usize,
    },
    Table {
        headers: Vec<String>,
        rows: Vec<Vec<String>>,
        line: usize,
    },
    Directive {
        name: String,
        args: Vec<String>,
        options: HashMap<String, String>,
        content: String,
        line: usize,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MarkdownNode {
    Heading {
        text: String,
        level: usize,
        line: usize,
    },
    Paragraph {
        content: String,
        line: usize,
    },
    CodeBlock {
        language: Option<String>,
        content: String,
        line: usize,
    },
    List {
        items: Vec<String>,
        ordered: bool,
        line: usize,
    },
    Table {
        headers: Vec<String>,
        rows: Vec<Vec<String>>,
        line: usize,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RstDirective {
    /// Directive name (e.g., "code-block", "toctree", "autoclass")
    pub name: String,

    /// Directive arguments
    pub args: Vec<String>,

    /// Directive options
    pub options: HashMap<String, String>,

    /// Directive content
    pub content: String,

    /// Line number where directive starts
    pub line: usize,
}

impl Document {
    pub fn new(source_path: PathBuf, output_path: PathBuf) -> Self {
        Self {
            source_path,
            output_path,
            title: String::new(),
            content: DocumentContent::PlainText(String::new()),
            metadata: DocumentMetadata::default(),
            html: String::new(),
            source_mtime: Utc::now(),
            build_time: Utc::now(),
            cross_refs: Vec::new(),
            toc: Vec::new(),
            toctrees: Vec::new(),
            directive_records: Vec::new(),
            role_records: Vec::new(),
            labels: Vec::new(),
            registry: crate::rst::RegistryExport::default(),
        }
    }

    #[allow(dead_code)]
    pub fn set_title(&mut self, title: String) {
        self.title = title;
    }

    #[allow(dead_code)]
    pub fn add_cross_ref(&mut self, cross_ref: CrossReference) {
        self.cross_refs.push(cross_ref);
    }

    #[allow(dead_code)]
    pub fn add_toc_entry(&mut self, entry: TocEntry) {
        self.toc.push(entry);
    }

    #[allow(dead_code)]
    pub fn set_html(&mut self, html: String) {
        self.html = html;
        self.build_time = Utc::now();
    }
}

impl TocEntry {
    pub fn new(title: String, level: usize, anchor: String, line_number: usize) -> Self {
        Self {
            title,
            level,
            anchor,
            line_number,
            children: Vec::new(),
        }
    }

    #[allow(dead_code)]
    pub fn add_child(&mut self, child: TocEntry) {
        self.children.push(child);
    }
}