Skip to main content

rdx_ast/
lib.rs

1use serde::{Deserialize, Serialize};
2
3/// Positional data mapping an AST node back to its source `.rdx` file.
4/// Line and column numbers are 1-indexed. Offsets are 0-indexed byte offsets.
5#[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/// The root of an RDX document.
19#[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/// A union of all possible RDX nodes.
35#[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/// A standard CommonMark block node.
91#[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    /// For list items: whether a task list checkbox is checked.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub checked: Option<bool>,
100    /// For headings: an explicit ID attribute (`# Title {#my-id}`).
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub id: Option<String>,
103    pub children: Vec<Node>,
104    pub position: Position,
105}
106
107/// An RDX component node.
108#[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/// A single attribute with its own positional data.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct AttributeNode {
121    pub name: String,
122    pub value: AttributeValue,
123    pub position: Position,
124}
125
126/// Supported attribute value types.
127#[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/// A footnote node (definition or reference).
140#[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/// A link node with URL and optional title.
148#[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/// An image node with URL, optional title, and alt text.
158#[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/// A fenced code block with optional language and meta string.
170#[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/// A literal text node.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct TextNode {
183    pub value: String,
184    pub position: Position,
185}
186
187/// A variable interpolation node.
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct VariableNode {
190    pub path: String,
191    pub position: Position,
192}
193
194/// An explicit error node for host-level error boundaries.
195#[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    /// Returns a mutable reference to this node's children, if it has any.
205    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    /// Returns a reference to this node's children, if it has any.
229    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}