1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct Position {
7 pub start: Point,
8 pub end: Point,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct Point {
13 pub line: usize,
14 pub column: usize,
15 pub offset: usize,
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct Root {
21 #[serde(rename = "type")]
22 pub node_type: RootType,
23 pub frontmatter: Option<serde_json::Value>,
24 pub children: Vec<Node>,
25 pub position: Position,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum RootType {
30 #[serde(rename = "root")]
31 Root,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(tag = "type")]
37pub enum Node {
38 #[serde(rename = "text")]
39 Text(TextNode),
40 #[serde(rename = "code_inline")]
41 CodeInline(TextNode),
42 #[serde(rename = "code_block")]
43 CodeBlock(CodeBlockNode),
44 #[serde(rename = "paragraph")]
45 Paragraph(StandardBlockNode),
46 #[serde(rename = "heading")]
47 Heading(StandardBlockNode),
48 #[serde(rename = "list")]
49 List(StandardBlockNode),
50 #[serde(rename = "list_item")]
51 ListItem(StandardBlockNode),
52 #[serde(rename = "blockquote")]
53 Blockquote(StandardBlockNode),
54 #[serde(rename = "thematic_break")]
55 ThematicBreak(StandardBlockNode),
56 #[serde(rename = "html")]
57 Html(StandardBlockNode),
58 #[serde(rename = "table")]
59 Table(StandardBlockNode),
60 #[serde(rename = "table_row")]
61 TableRow(StandardBlockNode),
62 #[serde(rename = "table_cell")]
63 TableCell(StandardBlockNode),
64 #[serde(rename = "link")]
65 Link(LinkNode),
66 #[serde(rename = "image")]
67 Image(ImageNode),
68 #[serde(rename = "emphasis")]
69 Emphasis(StandardBlockNode),
70 #[serde(rename = "strong")]
71 Strong(StandardBlockNode),
72 #[serde(rename = "strikethrough")]
73 Strikethrough(StandardBlockNode),
74 #[serde(rename = "footnote_definition")]
75 FootnoteDefinition(FootnoteNode),
76 #[serde(rename = "footnote_reference")]
77 FootnoteReference(FootnoteNode),
78 #[serde(rename = "math_inline")]
79 MathInline(TextNode),
80 #[serde(rename = "math_display")]
81 MathDisplay(TextNode),
82 #[serde(rename = "component")]
83 Component(ComponentNode),
84 #[serde(rename = "variable")]
85 Variable(VariableNode),
86 #[serde(rename = "error")]
87 Error(ErrorNode),
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct StandardBlockNode {
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub depth: Option<u8>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub ordered: Option<bool>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub checked: Option<bool>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub id: Option<String>,
103 pub children: Vec<Node>,
104 pub position: Position,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct ComponentNode {
110 pub name: String,
111 #[serde(rename = "isInline")]
112 pub is_inline: bool,
113 pub attributes: Vec<AttributeNode>,
114 pub children: Vec<Node>,
115 pub position: Position,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct AttributeNode {
121 pub name: String,
122 pub value: AttributeValue,
123 pub position: Position,
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[serde(untagged)]
129pub enum AttributeValue {
130 Null,
131 Bool(bool),
132 Number(serde_json::Number),
133 String(String),
134 Array(Vec<serde_json::Value>),
135 Object(serde_json::Map<String, serde_json::Value>),
136 Variable(VariableNode),
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct FootnoteNode {
142 pub label: String,
143 pub children: Vec<Node>,
144 pub position: Position,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct LinkNode {
150 pub url: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub title: Option<String>,
153 pub children: Vec<Node>,
154 pub position: Position,
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ImageNode {
160 pub url: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub title: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub alt: Option<String>,
165 pub children: Vec<Node>,
166 pub position: Position,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct CodeBlockNode {
172 pub value: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub lang: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub meta: Option<String>,
177 pub position: Position,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct TextNode {
183 pub value: String,
184 pub position: Position,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct VariableNode {
190 pub path: String,
191 pub position: Position,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct ErrorNode {
197 pub message: String,
198 #[serde(rename = "rawContent")]
199 pub raw_content: String,
200 pub position: Position,
201}
202
203impl Node {
204 pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
206 match self {
207 Node::Paragraph(b)
208 | Node::Heading(b)
209 | Node::List(b)
210 | Node::ListItem(b)
211 | Node::Blockquote(b)
212 | Node::Html(b)
213 | Node::Table(b)
214 | Node::TableRow(b)
215 | Node::TableCell(b)
216 | Node::Emphasis(b)
217 | Node::Strong(b)
218 | Node::Strikethrough(b)
219 | Node::ThematicBreak(b) => Some(&mut b.children),
220 Node::Link(l) => Some(&mut l.children),
221 Node::Image(i) => Some(&mut i.children),
222 Node::Component(c) => Some(&mut c.children),
223 Node::FootnoteDefinition(n) => Some(&mut n.children),
224 _ => None,
225 }
226 }
227
228 pub fn children(&self) -> Option<&[Node]> {
230 match self {
231 Node::Paragraph(b)
232 | Node::Heading(b)
233 | Node::List(b)
234 | Node::ListItem(b)
235 | Node::Blockquote(b)
236 | Node::Html(b)
237 | Node::Table(b)
238 | Node::TableRow(b)
239 | Node::TableCell(b)
240 | Node::Emphasis(b)
241 | Node::Strong(b)
242 | Node::Strikethrough(b)
243 | Node::ThematicBreak(b) => Some(&b.children),
244 Node::Link(l) => Some(&l.children),
245 Node::Image(i) => Some(&i.children),
246 Node::Component(c) => Some(&c.children),
247 Node::FootnoteDefinition(n) => Some(&n.children),
248 _ => None,
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn pos(line: usize, col: usize, off: usize) -> Point {
258 Point {
259 line,
260 column: col,
261 offset: off,
262 }
263 }
264
265 fn span(sl: usize, sc: usize, so: usize, el: usize, ec: usize, eo: usize) -> Position {
266 Position {
267 start: pos(sl, sc, so),
268 end: pos(el, ec, eo),
269 }
270 }
271
272 #[test]
273 fn root_serializes_type_field() {
274 let root = Root {
275 node_type: RootType::Root,
276 frontmatter: None,
277 children: vec![],
278 position: span(1, 1, 0, 1, 1, 0),
279 };
280 let json = serde_json::to_value(&root).unwrap();
281 assert_eq!(json["type"], "root");
282 assert!(json["frontmatter"].is_null());
283 assert_eq!(json["children"], serde_json::json!([]));
284 }
285
286 #[test]
287 fn component_node_serializes_correctly() {
288 let node = Node::Component(ComponentNode {
289 name: "Badge".into(),
290 is_inline: false,
291 attributes: vec![
292 AttributeNode {
293 name: "status".into(),
294 value: AttributeValue::String("beta".into()),
295 position: span(1, 8, 7, 1, 22, 21),
296 },
297 AttributeNode {
298 name: "active".into(),
299 value: AttributeValue::Bool(true),
300 position: span(1, 23, 22, 1, 36, 35),
301 },
302 ],
303 children: vec![Node::Text(TextNode {
304 value: "New Feature".into(),
305 position: span(1, 37, 36, 1, 48, 47),
306 })],
307 position: span(1, 1, 0, 1, 55, 54),
308 });
309
310 let json = serde_json::to_value(&node).unwrap();
311 assert_eq!(json["type"], "component");
312 assert_eq!(json["name"], "Badge");
313 assert_eq!(json["isInline"], false);
314 assert_eq!(json["attributes"][0]["name"], "status");
315 assert_eq!(json["attributes"][0]["value"], "beta");
316 assert_eq!(json["attributes"][1]["name"], "active");
317 assert_eq!(json["attributes"][1]["value"], true);
318 assert_eq!(json["children"][0]["type"], "text");
319 assert_eq!(json["children"][0]["value"], "New Feature");
320 }
321
322 #[test]
323 fn attribute_value_null_serializes_to_null() {
324 let val = AttributeValue::Null;
325 let json = serde_json::to_value(&val).unwrap();
326 assert!(json.is_null());
327 }
328
329 #[test]
330 fn attribute_value_number() {
331 let val = AttributeValue::Number(serde_json::Number::from(42));
332 let json = serde_json::to_value(&val).unwrap();
333 assert_eq!(json, 42);
334 }
335
336 #[test]
337 fn attribute_value_json_object() {
338 let mut map = serde_json::Map::new();
339 map.insert("type".into(), serde_json::Value::String("bar".into()));
340 let val = AttributeValue::Object(map);
341 let json = serde_json::to_value(&val).unwrap();
342 assert_eq!(json["type"], "bar");
343 }
344
345 #[test]
346 fn attribute_value_json_array() {
347 let val = AttributeValue::Array(vec![
348 serde_json::Value::from(10),
349 serde_json::Value::from(20),
350 ]);
351 let json = serde_json::to_value(&val).unwrap();
352 assert_eq!(json, serde_json::json!([10, 20]));
353 }
354
355 #[test]
356 fn variable_node_serializes() {
357 let node = Node::Variable(VariableNode {
358 path: "frontmatter.title".into(),
359 position: span(1, 1, 0, 1, 20, 19),
360 });
361 let json = serde_json::to_value(&node).unwrap();
362 assert_eq!(json["type"], "variable");
363 assert_eq!(json["path"], "frontmatter.title");
364 }
365
366 #[test]
367 fn error_node_serializes() {
368 let node = Node::Error(ErrorNode {
369 message: "Unclosed tag".into(),
370 raw_content: "<Notice>".into(),
371 position: span(1, 1, 0, 1, 9, 8),
372 });
373 let json = serde_json::to_value(&node).unwrap();
374 assert_eq!(json["type"], "error");
375 assert_eq!(json["message"], "Unclosed tag");
376 assert_eq!(json["rawContent"], "<Notice>");
377 }
378
379 #[test]
380 fn standard_block_omits_none_fields() {
381 let node = Node::Heading(StandardBlockNode {
382 depth: Some(2),
383 ordered: None,
384 checked: None,
385 id: None,
386 children: vec![],
387 position: span(1, 1, 0, 1, 10, 9),
388 });
389 let json = serde_json::to_value(&node).unwrap();
390 assert_eq!(json["depth"], 2);
391 assert!(json.get("ordered").is_none());
392 assert!(json.get("checked").is_none());
393 assert!(json.get("id").is_none());
394 }
395
396 #[test]
397 fn roundtrip_component_node() {
398 let original = Node::Component(ComponentNode {
399 name: "Chart".into(),
400 is_inline: true,
401 attributes: vec![],
402 children: vec![],
403 position: span(1, 1, 0, 1, 10, 9),
404 });
405 let serialized = serde_json::to_string(&original).unwrap();
406 let deserialized: Node = serde_json::from_str(&serialized).unwrap();
407 assert_eq!(original, deserialized);
408 }
409}