1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6fn 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 #[serde(
26 serialize_with = "serialize_pathbuf",
27 deserialize_with = "deserialize_pathbuf"
28 )]
29 pub source_path: PathBuf,
30
31 #[serde(
33 serialize_with = "serialize_pathbuf",
34 deserialize_with = "deserialize_pathbuf"
35 )]
36 pub output_path: PathBuf,
37
38 pub title: String,
40
41 pub content: DocumentContent,
43
44 pub metadata: DocumentMetadata,
46
47 pub html: String,
49
50 pub source_mtime: DateTime<Utc>,
52
53 pub build_time: DateTime<Utc>,
55
56 pub cross_refs: Vec<CrossReference>,
58
59 pub toc: Vec<TocEntry>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum DocumentContent {
65 RestructuredText(RstContent),
66 Markdown(MarkdownContent),
67 PlainText(String),
68}
69
70impl std::fmt::Display for DocumentContent {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 DocumentContent::RestructuredText(rst) => write!(f, "{}", rst.raw),
74 DocumentContent::Markdown(md) => write!(f, "{}", md.raw),
75 DocumentContent::PlainText(text) => write!(f, "{}", text),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct RstContent {
82 pub raw: String,
84
85 pub ast: Vec<RstNode>,
87
88 pub directives: Vec<RstDirective>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct MarkdownContent {
94 pub raw: String,
96
97 pub ast: Vec<MarkdownNode>,
99
100 pub front_matter: Option<serde_yaml::Value>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, Default)]
105pub struct DocumentMetadata {
106 pub authors: Vec<String>,
108
109 pub created: Option<DateTime<Utc>>,
111
112 pub modified: Option<DateTime<Utc>>,
114
115 pub tags: Vec<String>,
117
118 pub category: Option<String>,
120
121 pub custom: HashMap<String, serde_json::Value>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct CrossReference {
127 pub ref_type: String,
129
130 pub target: String,
132
133 pub text: Option<String>,
135
136 pub line_number: usize,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct TocEntry {
142 pub title: String,
144
145 pub level: usize,
147
148 pub anchor: String,
150
151 pub line_number: usize,
153
154 pub children: Vec<TocEntry>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub enum RstNode {
160 Title {
161 text: String,
162 level: usize,
163 line: usize,
164 },
165 Paragraph {
166 content: String,
167 line: usize,
168 },
169 CodeBlock {
170 language: Option<String>,
171 content: String,
172 line: usize,
173 },
174 List {
175 items: Vec<String>,
176 ordered: bool,
177 line: usize,
178 },
179 Table {
180 headers: Vec<String>,
181 rows: Vec<Vec<String>>,
182 line: usize,
183 },
184 Directive {
185 name: String,
186 args: Vec<String>,
187 options: HashMap<String, String>,
188 content: String,
189 line: usize,
190 },
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub enum MarkdownNode {
195 Heading {
196 text: String,
197 level: usize,
198 line: usize,
199 },
200 Paragraph {
201 content: String,
202 line: usize,
203 },
204 CodeBlock {
205 language: Option<String>,
206 content: String,
207 line: usize,
208 },
209 List {
210 items: Vec<String>,
211 ordered: bool,
212 line: usize,
213 },
214 Table {
215 headers: Vec<String>,
216 rows: Vec<Vec<String>>,
217 line: usize,
218 },
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct RstDirective {
223 pub name: String,
225
226 pub args: Vec<String>,
228
229 pub options: HashMap<String, String>,
231
232 pub content: String,
234
235 pub line: usize,
237}
238
239impl Document {
240 pub fn new(source_path: PathBuf, output_path: PathBuf) -> Self {
241 Self {
242 source_path,
243 output_path,
244 title: String::new(),
245 content: DocumentContent::PlainText(String::new()),
246 metadata: DocumentMetadata::default(),
247 html: String::new(),
248 source_mtime: Utc::now(),
249 build_time: Utc::now(),
250 cross_refs: Vec::new(),
251 toc: Vec::new(),
252 }
253 }
254
255 #[allow(dead_code)]
256 pub fn set_title(&mut self, title: String) {
257 self.title = title;
258 }
259
260 #[allow(dead_code)]
261 pub fn add_cross_ref(&mut self, cross_ref: CrossReference) {
262 self.cross_refs.push(cross_ref);
263 }
264
265 #[allow(dead_code)]
266 pub fn add_toc_entry(&mut self, entry: TocEntry) {
267 self.toc.push(entry);
268 }
269
270 #[allow(dead_code)]
271 pub fn set_html(&mut self, html: String) {
272 self.html = html;
273 self.build_time = Utc::now();
274 }
275}
276
277impl TocEntry {
278 pub fn new(title: String, level: usize, anchor: String, line_number: usize) -> Self {
279 Self {
280 title,
281 level,
282 anchor,
283 line_number,
284 children: Vec::new(),
285 }
286 }
287
288 #[allow(dead_code)]
289 pub fn add_child(&mut self, child: TocEntry) {
290 self.children.push(child);
291 }
292}