Skip to main content

sphinx_ultra/
document.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6// Custom serialization for PathBuf to handle cross-platform compatibility
7fn serialize_pathbuf<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
8where
9    S: Serializer,
10{
11    serializer.serialize_str(&path.to_string_lossy())
12}
13
14fn deserialize_pathbuf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
15where
16    D: Deserializer<'de>,
17{
18    let s = String::deserialize(deserializer)?;
19    Ok(PathBuf::from(s))
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Document {
24    /// Source file path
25    #[serde(
26        serialize_with = "serialize_pathbuf",
27        deserialize_with = "deserialize_pathbuf"
28    )]
29    pub source_path: PathBuf,
30
31    /// Output file path
32    #[serde(
33        serialize_with = "serialize_pathbuf",
34        deserialize_with = "deserialize_pathbuf"
35    )]
36    pub output_path: PathBuf,
37
38    /// Document title
39    pub title: String,
40
41    /// Document content (parsed)
42    pub content: DocumentContent,
43
44    /// Document metadata
45    pub metadata: DocumentMetadata,
46
47    /// Rendered HTML content
48    pub html: String,
49
50    /// Source file modification time
51    pub source_mtime: DateTime<Utc>,
52
53    /// Build time
54    pub build_time: DateTime<Utc>,
55
56    /// Cross-references found in this document
57    pub cross_refs: Vec<CrossReference>,
58
59    /// Table of contents
60    pub toc: Vec<TocEntry>,
61
62    /// Toctree directives found in this document (wave 3: derived from
63    /// the doctree at parse time; the build resolves/warns).
64    pub toctrees: Vec<crate::rst::ToctreeRecord>,
65
66    /// Directive occurrences for the validation system.
67    pub directive_records: Vec<crate::rst::DirectiveRecord>,
68
69    /// Role occurrences for validation + nitpicky cross-ref checking.
70    pub role_records: Vec<crate::rst::RoleRecord>,
71
72    /// Explicit hyperlink-target labels (docutils-normalized names).
73    pub labels: Vec<LabelRecord>,
74
75    /// The parse's id/name registry snapshot (docutils `document.nameids`),
76    /// which the std-domain label harvest consumes. It rides the `Document`
77    /// — rather than being a separate read-phase value — so an incremental
78    /// cache hit, which skips parsing entirely, still has the real registry
79    /// instead of an empty stand-in. Deliberately *not* `#[serde(default)]`:
80    /// a cache entry written before this field existed fails to decode and
81    /// is re-parsed, which is the honest outcome.
82    pub registry: crate::rst::RegistryExport,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct LabelRecord {
87    pub name: String,
88    /// 1-based source line of the target marker.
89    pub line: usize,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub enum DocumentContent {
94    RestructuredText(RstContent),
95    Markdown(MarkdownContent),
96    PlainText(String),
97}
98
99impl std::fmt::Display for DocumentContent {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            DocumentContent::RestructuredText(rst) => write!(f, "{}", rst.raw),
103            DocumentContent::Markdown(md) => write!(f, "{}", md.raw),
104            DocumentContent::PlainText(text) => write!(f, "{}", text),
105        }
106    }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct RstContent {
111    /// Raw RST content
112    pub raw: String,
113
114    /// Parsed AST
115    pub ast: Vec<RstNode>,
116
117    /// Directives found in the document
118    pub directives: Vec<RstDirective>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct MarkdownContent {
123    /// Raw Markdown content
124    pub raw: String,
125
126    /// Parsed AST
127    pub ast: Vec<MarkdownNode>,
128
129    /// Front matter
130    pub front_matter: Option<serde_yaml::Value>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, Default)]
134pub struct DocumentMetadata {
135    /// Document author(s)
136    pub authors: Vec<String>,
137
138    /// Document creation date
139    pub created: Option<DateTime<Utc>>,
140
141    /// Document last modified date
142    pub modified: Option<DateTime<Utc>>,
143
144    /// Document tags
145    pub tags: Vec<String>,
146
147    /// Document category
148    pub category: Option<String>,
149
150    /// Custom metadata fields
151    pub custom: HashMap<String, serde_json::Value>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct CrossReference {
156    /// Reference type (doc, ref, func, class, etc.)
157    pub ref_type: String,
158
159    /// Reference target
160    pub target: String,
161
162    /// Reference text
163    pub text: Option<String>,
164
165    /// Line number where reference appears
166    pub line_number: usize,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TocEntry {
171    /// Entry title
172    pub title: String,
173
174    /// Entry level (1-6)
175    pub level: usize,
176
177    /// Anchor ID
178    pub anchor: String,
179
180    /// Line number
181    pub line_number: usize,
182
183    /// Child entries
184    pub children: Vec<TocEntry>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub enum RstNode {
189    Title {
190        text: String,
191        level: usize,
192        line: usize,
193    },
194    Paragraph {
195        content: String,
196        line: usize,
197    },
198    CodeBlock {
199        language: Option<String>,
200        content: String,
201        line: usize,
202    },
203    List {
204        items: Vec<String>,
205        ordered: bool,
206        line: usize,
207    },
208    Table {
209        headers: Vec<String>,
210        rows: Vec<Vec<String>>,
211        line: usize,
212    },
213    Directive {
214        name: String,
215        args: Vec<String>,
216        options: HashMap<String, String>,
217        content: String,
218        line: usize,
219    },
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub enum MarkdownNode {
224    Heading {
225        text: String,
226        level: usize,
227        line: usize,
228    },
229    Paragraph {
230        content: String,
231        line: usize,
232    },
233    CodeBlock {
234        language: Option<String>,
235        content: String,
236        line: usize,
237    },
238    List {
239        items: Vec<String>,
240        ordered: bool,
241        line: usize,
242    },
243    Table {
244        headers: Vec<String>,
245        rows: Vec<Vec<String>>,
246        line: usize,
247    },
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct RstDirective {
252    /// Directive name (e.g., "code-block", "toctree", "autoclass")
253    pub name: String,
254
255    /// Directive arguments
256    pub args: Vec<String>,
257
258    /// Directive options
259    pub options: HashMap<String, String>,
260
261    /// Directive content
262    pub content: String,
263
264    /// Line number where directive starts
265    pub line: usize,
266}
267
268impl Document {
269    pub fn new(source_path: PathBuf, output_path: PathBuf) -> Self {
270        Self {
271            source_path,
272            output_path,
273            title: String::new(),
274            content: DocumentContent::PlainText(String::new()),
275            metadata: DocumentMetadata::default(),
276            html: String::new(),
277            source_mtime: Utc::now(),
278            build_time: Utc::now(),
279            cross_refs: Vec::new(),
280            toc: Vec::new(),
281            toctrees: Vec::new(),
282            directive_records: Vec::new(),
283            role_records: Vec::new(),
284            labels: Vec::new(),
285            registry: crate::rst::RegistryExport::default(),
286        }
287    }
288
289    #[allow(dead_code)]
290    pub fn set_title(&mut self, title: String) {
291        self.title = title;
292    }
293
294    #[allow(dead_code)]
295    pub fn add_cross_ref(&mut self, cross_ref: CrossReference) {
296        self.cross_refs.push(cross_ref);
297    }
298
299    #[allow(dead_code)]
300    pub fn add_toc_entry(&mut self, entry: TocEntry) {
301        self.toc.push(entry);
302    }
303
304    #[allow(dead_code)]
305    pub fn set_html(&mut self, html: String) {
306        self.html = html;
307        self.build_time = Utc::now();
308    }
309}
310
311impl TocEntry {
312    pub fn new(title: String, level: usize, anchor: String, line_number: usize) -> Self {
313        Self {
314            title,
315            level,
316            anchor,
317            line_number,
318            children: Vec::new(),
319        }
320    }
321
322    #[allow(dead_code)]
323    pub fn add_child(&mut self, child: TocEntry) {
324        self.children.push(child);
325    }
326}