1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct Document {
13 pub blocks: Vec<Block>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub enum Block {
22 Heading {
24 level: u8,
25 title: Title,
26 tags: Vec<Tag>,
27 children: Vec<Block>,
28 },
29 Paragraph {
30 inlines: Vec<Inline>,
31 },
32 SrcBlock {
34 language: String,
35 content: String,
36 },
37 ExampleBlock {
39 content: String,
40 },
41 QuoteBlock {
42 children: Vec<Block>,
43 },
44 List {
45 list_type: ListType,
46 items: Vec<ListItem>,
47 },
48 Table {
49 rows: Vec<Vec<TableCell>>,
50 },
51 PropertyDrawer {
52 entries: Vec<(String, String)>,
53 },
54 LogbookDrawer {
55 entries: Vec<LogEntry>,
56 },
57 Planning {
58 entries: Vec<PlanningEntry>,
59 },
60 Comment {
61 text: String,
62 },
63 Keyword {
69 name: String,
70 value: String,
71 },
72 BlankLine,
75 HorizontalRule,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub enum ListType {
81 Ordered(u64),
84 Unordered,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct ListItem {
90 pub content: Vec<Block>,
91 pub checkbox: Checkbox,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum Checkbox {
97 NoCheckbox,
98 Unchecked,
99 Checked,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct TableCell {
105 pub inlines: Vec<Inline>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub enum PlanningEntry {
111 Scheduled(Timestamp),
112 Deadline(Timestamp),
113 Closed(Timestamp),
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct LogEntry {
119 pub timestamp: Timestamp,
120 pub note: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub enum Inline {
126 Plain(String),
127 Bold(Vec<Inline>),
128 Italic(Vec<Inline>),
129 Strikethrough(Vec<Inline>),
131 InlineCode(String),
132 Verbatim(String),
133 LineBreak,
136 Link {
139 target: String,
140 description: Option<String>,
141 },
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
148pub struct NodeId(pub String);
149
150#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
152pub struct Title(pub String);
153
154#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
156pub struct Tag(pub String);
157
158impl NodeId {
159 #[must_use]
161 pub fn as_str(&self) -> &str {
162 &self.0
163 }
164}
165
166impl Title {
167 #[must_use]
169 pub fn as_str(&self) -> &str {
170 &self.0
171 }
172}
173
174impl Tag {
175 #[must_use]
177 pub fn as_str(&self) -> &str {
178 &self.0
179 }
180}
181
182impl From<String> for NodeId {
183 fn from(s: String) -> Self {
184 Self(s)
185 }
186}
187
188impl From<&str> for NodeId {
189 fn from(s: &str) -> Self {
190 Self(s.to_owned())
191 }
192}
193
194impl From<String> for Title {
195 fn from(s: String) -> Self {
196 Self(s)
197 }
198}
199
200impl From<&str> for Title {
201 fn from(s: &str) -> Self {
202 Self(s.to_owned())
203 }
204}
205
206impl From<String> for Tag {
207 fn from(s: String) -> Self {
208 Self(s)
209 }
210}
211
212impl From<&str> for Tag {
213 fn from(s: &str) -> Self {
214 Self(s.to_owned())
215 }
216}
217
218impl std::fmt::Display for NodeId {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.write_str(&self.0)
221 }
222}
223
224impl std::fmt::Display for Title {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.write_str(&self.0)
227 }
228}
229
230impl std::fmt::Display for Tag {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 f.write_str(&self.0)
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
238pub struct Timestamp(pub String);
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn construct_simple_document() {
246 let doc = Document {
247 blocks: vec![Block::Heading {
248 level: 1,
249 title: Title("Hello".into()),
250 tags: vec![],
251 children: vec![Block::Paragraph {
252 inlines: vec![Inline::Plain("some text".into())],
253 }],
254 }],
255 };
256 assert_eq!(doc.blocks.len(), 1);
257 }
258
259 #[test]
260 fn ast_types_serialize_roundtrip() {
261 let doc = Document {
262 blocks: vec![
263 Block::Heading {
264 level: 1,
265 title: Title("Test".into()),
266 tags: vec![Tag("rust".into())],
267 children: vec![
268 Block::Paragraph {
269 inlines: vec![
270 Inline::Plain("Hello ".into()),
271 Inline::Bold(vec![Inline::Plain("world".into())]),
272 Inline::Plain(". ".into()),
273 Inline::Link {
274 target: "https://example.com".into(),
275 description: Some("link".into()),
276 },
277 ],
278 },
279 Block::SrcBlock {
280 language: "rust".into(),
281 content: "fn main() {}".into(),
282 },
283 Block::PropertyDrawer {
284 entries: vec![("ID".into(), "abc-123".into())],
285 },
286 ],
287 },
288 Block::List {
289 list_type: ListType::Unordered,
290 items: vec![
291 ListItem {
292 content: vec![Block::Paragraph {
293 inlines: vec![Inline::Plain("first".into())],
294 }],
295 checkbox: Checkbox::NoCheckbox,
296 },
297 ListItem {
298 content: vec![Block::Paragraph {
299 inlines: vec![Inline::Plain("second".into())],
300 }],
301 checkbox: Checkbox::Checked,
302 },
303 ],
304 },
305 ],
306 };
307
308 let json = serde_json::to_string(&doc).unwrap();
309 let roundtripped: Document = serde_json::from_str(&json).unwrap();
310 assert_eq!(doc, roundtripped);
311 }
312}