Skip to main content

rdx_ast/
lib.rs

1use rkyv::Archive;
2use serde::{Deserialize, Serialize};
3
4/// rkyv wrapper that stores serde_json types as their JSON string representation.
5mod rkyv_json {
6    use rkyv::rancor::Fallible;
7    use rkyv::string::ArchivedString;
8    use rkyv::with::{ArchiveWith, DeserializeWith, SerializeWith};
9    use rkyv::{Archive, Place};
10
11    pub struct AsJsonString;
12
13    impl<T: serde::Serialize> ArchiveWith<T> for AsJsonString {
14        type Archived = ArchivedString;
15        type Resolver = <String as Archive>::Resolver;
16
17        fn resolve_with(field: &T, resolver: Self::Resolver, out: Place<Self::Archived>) {
18            // Safety: serde_json::to_string cannot fail for types that already implement Serialize
19            // (no IO, no map-key errors). An unwrap is appropriate here.
20            let json = serde_json::to_string(field).expect("serde_json::to_string failed");
21            ArchivedString::resolve_from_str(&json, resolver, out);
22        }
23    }
24
25    impl<T: serde::Serialize, S: Fallible<Error: rkyv::rancor::Source> + rkyv::ser::Writer + ?Sized>
26        SerializeWith<T, S> for AsJsonString
27    {
28        fn serialize_with(field: &T, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
29            // Safety: see resolve_with — serialization of valid Serialize types cannot fail.
30            let json = serde_json::to_string(field).expect("serde_json::to_string failed");
31            ArchivedString::serialize_from_str(&json, serializer)
32        }
33    }
34
35    impl<T: serde::de::DeserializeOwned, D: Fallible + ?Sized> DeserializeWith<ArchivedString, T, D>
36        for AsJsonString
37    {
38        fn deserialize_with(archived: &ArchivedString, _: &mut D) -> Result<T, D::Error> {
39            // Safety: the archived string was produced by to_string above, so from_str cannot fail.
40            Ok(serde_json::from_str(archived.as_str()).expect("serde_json::from_str failed"))
41        }
42    }
43}
44
45/// Positional data mapping an AST node back to its source `.rdx` file.
46/// Line and column numbers are 1-indexed. Offsets are 0-indexed byte offsets.
47#[derive(
48    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
49)]
50pub struct Position {
51    pub start: Point,
52    pub end: Point,
53}
54
55#[derive(
56    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
57)]
58pub struct Point {
59    pub line: usize,
60    pub column: usize,
61    pub offset: usize,
62}
63
64/// The root of an RDX document.
65#[derive(
66    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
67)]
68#[rkyv(serialize_bounds(
69    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
70))]
71#[rkyv(deserialize_bounds(
72    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
73))]
74#[rkyv(bytecheck(bounds(
75    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
76)))]
77pub struct Root {
78    #[serde(rename = "type")]
79    pub node_type: RootType,
80    #[rkyv(with = rkyv::with::Map<rkyv_json::AsJsonString>)]
81    pub frontmatter: Option<serde_json::Value>,
82    #[rkyv(omit_bounds)]
83    pub children: Vec<Node>,
84    pub position: Position,
85}
86
87#[derive(
88    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
89)]
90pub enum RootType {
91    #[serde(rename = "root")]
92    Root,
93}
94
95/// A union of all possible RDX nodes.
96#[derive(
97    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
98)]
99#[rkyv(serialize_bounds(
100    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
101))]
102#[rkyv(deserialize_bounds(
103    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
104))]
105#[rkyv(bytecheck(bounds(
106    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
107)))]
108#[serde(tag = "type")]
109pub enum Node {
110    #[serde(rename = "text")]
111    Text(#[rkyv(omit_bounds)] TextNode),
112    #[serde(rename = "code_inline")]
113    CodeInline(#[rkyv(omit_bounds)] TextNode),
114    #[serde(rename = "code_block")]
115    CodeBlock(#[rkyv(omit_bounds)] CodeBlockNode),
116    #[serde(rename = "paragraph")]
117    Paragraph(#[rkyv(omit_bounds)] StandardBlockNode),
118    #[serde(rename = "heading")]
119    Heading(#[rkyv(omit_bounds)] StandardBlockNode),
120    #[serde(rename = "list")]
121    List(#[rkyv(omit_bounds)] StandardBlockNode),
122    #[serde(rename = "list_item")]
123    ListItem(#[rkyv(omit_bounds)] StandardBlockNode),
124    #[serde(rename = "blockquote")]
125    Blockquote(#[rkyv(omit_bounds)] StandardBlockNode),
126    #[serde(rename = "thematic_break")]
127    ThematicBreak(#[rkyv(omit_bounds)] StandardBlockNode),
128    #[serde(rename = "html")]
129    Html(#[rkyv(omit_bounds)] StandardBlockNode),
130    #[serde(rename = "table")]
131    Table(#[rkyv(omit_bounds)] StandardBlockNode),
132    #[serde(rename = "table_row")]
133    TableRow(#[rkyv(omit_bounds)] StandardBlockNode),
134    #[serde(rename = "table_cell")]
135    TableCell(#[rkyv(omit_bounds)] StandardBlockNode),
136    #[serde(rename = "link")]
137    Link(#[rkyv(omit_bounds)] LinkNode),
138    #[serde(rename = "image")]
139    Image(#[rkyv(omit_bounds)] ImageNode),
140    #[serde(rename = "emphasis")]
141    Emphasis(#[rkyv(omit_bounds)] StandardBlockNode),
142    #[serde(rename = "strong")]
143    Strong(#[rkyv(omit_bounds)] StandardBlockNode),
144    #[serde(rename = "strikethrough")]
145    Strikethrough(#[rkyv(omit_bounds)] StandardBlockNode),
146    #[serde(rename = "footnote_definition")]
147    FootnoteDefinition(#[rkyv(omit_bounds)] FootnoteNode),
148    #[serde(rename = "footnote_reference")]
149    FootnoteReference(#[rkyv(omit_bounds)] FootnoteNode),
150    #[serde(rename = "math_inline")]
151    MathInline(#[rkyv(omit_bounds)] TextNode),
152    #[serde(rename = "math_display")]
153    MathDisplay(#[rkyv(omit_bounds)] TextNode),
154    #[serde(rename = "component")]
155    Component(#[rkyv(omit_bounds)] ComponentNode),
156    #[serde(rename = "variable")]
157    Variable(#[rkyv(omit_bounds)] VariableNode),
158    #[serde(rename = "error")]
159    Error(#[rkyv(omit_bounds)] ErrorNode),
160}
161
162/// A standard CommonMark block node.
163#[derive(
164    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
165)]
166#[rkyv(serialize_bounds(
167    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
168))]
169#[rkyv(deserialize_bounds(
170    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
171))]
172#[rkyv(bytecheck(bounds(
173    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
174)))]
175pub struct StandardBlockNode {
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub depth: Option<u8>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub ordered: Option<bool>,
180    /// For list items: whether a task list checkbox is checked.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub checked: Option<bool>,
183    /// For headings: an explicit ID attribute (`# Title {#my-id}`).
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub id: Option<String>,
186    #[rkyv(omit_bounds)]
187    pub children: Vec<Node>,
188    pub position: Position,
189}
190
191/// An RDX component node.
192#[derive(
193    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
194)]
195#[rkyv(serialize_bounds(
196    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
197))]
198#[rkyv(deserialize_bounds(
199    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
200))]
201#[rkyv(bytecheck(bounds(
202    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
203)))]
204pub struct ComponentNode {
205    pub name: String,
206    #[serde(rename = "isInline")]
207    pub is_inline: bool,
208    #[rkyv(omit_bounds)]
209    pub attributes: Vec<AttributeNode>,
210    #[rkyv(omit_bounds)]
211    pub children: Vec<Node>,
212    /// Raw source text of the component body (between open/close tags).
213    /// Preserved verbatim for components that need whitespace-sensitive content
214    /// (e.g. CodeBlock). Empty for self-closing components.
215    #[serde(
216        default,
217        rename = "rawContent",
218        skip_serializing_if = "String::is_empty"
219    )]
220    pub raw_content: String,
221    pub position: Position,
222}
223
224/// A single attribute with its own positional data.
225#[derive(
226    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
227)]
228#[rkyv(serialize_bounds(
229    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
230))]
231#[rkyv(deserialize_bounds(
232    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
233))]
234#[rkyv(bytecheck(bounds(
235    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
236)))]
237pub struct AttributeNode {
238    pub name: String,
239    #[rkyv(omit_bounds)]
240    pub value: AttributeValue,
241    pub position: Position,
242}
243
244/// Supported attribute value types.
245#[derive(
246    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
247)]
248#[rkyv(serialize_bounds(
249    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
250))]
251#[rkyv(deserialize_bounds(
252    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
253))]
254#[rkyv(bytecheck(bounds(
255    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
256)))]
257#[serde(untagged)]
258pub enum AttributeValue {
259    Null,
260    Bool(bool),
261    Number(#[rkyv(with = rkyv_json::AsJsonString)] serde_json::Number),
262    String(String),
263    Array(#[rkyv(with = rkyv_json::AsJsonString)] Vec<serde_json::Value>),
264    Object(#[rkyv(with = rkyv_json::AsJsonString)] serde_json::Map<String, serde_json::Value>),
265    Variable(#[rkyv(omit_bounds)] VariableNode),
266}
267
268/// A footnote node (definition or reference).
269#[derive(
270    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
271)]
272#[rkyv(serialize_bounds(
273    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
274))]
275#[rkyv(deserialize_bounds(
276    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
277))]
278#[rkyv(bytecheck(bounds(
279    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
280)))]
281pub struct FootnoteNode {
282    pub label: String,
283    #[rkyv(omit_bounds)]
284    pub children: Vec<Node>,
285    pub position: Position,
286}
287
288/// A link node with URL and optional title.
289#[derive(
290    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
291)]
292#[rkyv(serialize_bounds(
293    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
294))]
295#[rkyv(deserialize_bounds(
296    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
297))]
298#[rkyv(bytecheck(bounds(
299    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
300)))]
301pub struct LinkNode {
302    pub url: String,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub title: Option<String>,
305    #[rkyv(omit_bounds)]
306    pub children: Vec<Node>,
307    pub position: Position,
308}
309
310/// An image node with URL, optional title, and alt text.
311#[derive(
312    Debug, Clone, PartialEq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
313)]
314#[rkyv(serialize_bounds(
315    __S: rkyv::ser::Writer + rkyv::ser::Allocator + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
316))]
317#[rkyv(deserialize_bounds(
318    __D: rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
319))]
320#[rkyv(bytecheck(bounds(
321    __C: rkyv::validation::ArchiveContext + rkyv::rancor::Fallible<Error: rkyv::rancor::Source>,
322)))]
323pub struct ImageNode {
324    pub url: String,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub title: Option<String>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub alt: Option<String>,
329    #[rkyv(omit_bounds)]
330    pub children: Vec<Node>,
331    pub position: Position,
332}
333
334/// A fenced code block with optional language and meta string.
335#[derive(
336    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
337)]
338pub struct CodeBlockNode {
339    pub value: String,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub lang: Option<String>,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub meta: Option<String>,
344    pub position: Position,
345}
346
347/// A literal text node.
348#[derive(
349    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
350)]
351pub struct TextNode {
352    pub value: String,
353    pub position: Position,
354}
355
356/// A variable interpolation node.
357#[derive(
358    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
359)]
360pub struct VariableNode {
361    pub path: String,
362    pub position: Position,
363}
364
365/// An explicit error node for host-level error boundaries.
366#[derive(
367    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
368)]
369pub struct ErrorNode {
370    pub message: String,
371    #[serde(rename = "rawContent")]
372    pub raw_content: String,
373    pub position: Position,
374}
375
376impl Node {
377    /// Returns a mutable reference to this node's children, if it has any.
378    pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
379        match self {
380            Node::Paragraph(b)
381            | Node::Heading(b)
382            | Node::List(b)
383            | Node::ListItem(b)
384            | Node::Blockquote(b)
385            | Node::Html(b)
386            | Node::Table(b)
387            | Node::TableRow(b)
388            | Node::TableCell(b)
389            | Node::Emphasis(b)
390            | Node::Strong(b)
391            | Node::Strikethrough(b)
392            | Node::ThematicBreak(b) => Some(&mut b.children),
393            Node::Link(l) => Some(&mut l.children),
394            Node::Image(i) => Some(&mut i.children),
395            Node::Component(c) => Some(&mut c.children),
396            Node::FootnoteDefinition(n) => Some(&mut n.children),
397            _ => None,
398        }
399    }
400
401    /// Returns a reference to this node's children, if it has any.
402    pub fn children(&self) -> Option<&[Node]> {
403        match self {
404            Node::Paragraph(b)
405            | Node::Heading(b)
406            | Node::List(b)
407            | Node::ListItem(b)
408            | Node::Blockquote(b)
409            | Node::Html(b)
410            | Node::Table(b)
411            | Node::TableRow(b)
412            | Node::TableCell(b)
413            | Node::Emphasis(b)
414            | Node::Strong(b)
415            | Node::Strikethrough(b)
416            | Node::ThematicBreak(b) => Some(&b.children),
417            Node::Link(l) => Some(&l.children),
418            Node::Image(i) => Some(&i.children),
419            Node::Component(c) => Some(&c.children),
420            Node::FootnoteDefinition(n) => Some(&n.children),
421            _ => None,
422        }
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    fn pos(line: usize, col: usize, off: usize) -> Point {
431        Point {
432            line,
433            column: col,
434            offset: off,
435        }
436    }
437
438    fn span(sl: usize, sc: usize, so: usize, el: usize, ec: usize, eo: usize) -> Position {
439        Position {
440            start: pos(sl, sc, so),
441            end: pos(el, ec, eo),
442        }
443    }
444
445    #[test]
446    fn root_serializes_type_field() {
447        let root = Root {
448            node_type: RootType::Root,
449            frontmatter: None,
450            children: vec![],
451            position: span(1, 1, 0, 1, 1, 0),
452        };
453        let json = serde_json::to_value(&root).unwrap();
454        assert_eq!(json["type"], "root");
455        assert!(json["frontmatter"].is_null());
456        assert_eq!(json["children"], serde_json::json!([]));
457    }
458
459    #[test]
460    fn component_node_serializes_correctly() {
461        let node = Node::Component(ComponentNode {
462            name: "Badge".into(),
463            is_inline: false,
464            attributes: vec![
465                AttributeNode {
466                    name: "status".into(),
467                    value: AttributeValue::String("beta".into()),
468                    position: span(1, 8, 7, 1, 22, 21),
469                },
470                AttributeNode {
471                    name: "active".into(),
472                    value: AttributeValue::Bool(true),
473                    position: span(1, 23, 22, 1, 36, 35),
474                },
475            ],
476            children: vec![Node::Text(TextNode {
477                value: "New Feature".into(),
478                position: span(1, 37, 36, 1, 48, 47),
479            })],
480            raw_content: String::new(),
481            position: span(1, 1, 0, 1, 55, 54),
482        });
483
484        let json = serde_json::to_value(&node).unwrap();
485        assert_eq!(json["type"], "component");
486        assert_eq!(json["name"], "Badge");
487        assert_eq!(json["isInline"], false);
488        assert_eq!(json["attributes"][0]["name"], "status");
489        assert_eq!(json["attributes"][0]["value"], "beta");
490        assert_eq!(json["attributes"][1]["name"], "active");
491        assert_eq!(json["attributes"][1]["value"], true);
492        assert_eq!(json["children"][0]["type"], "text");
493        assert_eq!(json["children"][0]["value"], "New Feature");
494    }
495
496    #[test]
497    fn attribute_value_null_serializes_to_null() {
498        let val = AttributeValue::Null;
499        let json = serde_json::to_value(&val).unwrap();
500        assert!(json.is_null());
501    }
502
503    #[test]
504    fn attribute_value_number() {
505        let val = AttributeValue::Number(serde_json::Number::from(42));
506        let json = serde_json::to_value(&val).unwrap();
507        assert_eq!(json, 42);
508    }
509
510    #[test]
511    fn attribute_value_json_object() {
512        let mut map = serde_json::Map::new();
513        map.insert("type".into(), serde_json::Value::String("bar".into()));
514        let val = AttributeValue::Object(map);
515        let json = serde_json::to_value(&val).unwrap();
516        assert_eq!(json["type"], "bar");
517    }
518
519    #[test]
520    fn attribute_value_json_array() {
521        let val = AttributeValue::Array(vec![
522            serde_json::Value::from(10),
523            serde_json::Value::from(20),
524        ]);
525        let json = serde_json::to_value(&val).unwrap();
526        assert_eq!(json, serde_json::json!([10, 20]));
527    }
528
529    #[test]
530    fn variable_node_serializes() {
531        let node = Node::Variable(VariableNode {
532            path: "frontmatter.title".into(),
533            position: span(1, 1, 0, 1, 20, 19),
534        });
535        let json = serde_json::to_value(&node).unwrap();
536        assert_eq!(json["type"], "variable");
537        assert_eq!(json["path"], "frontmatter.title");
538    }
539
540    #[test]
541    fn error_node_serializes() {
542        let node = Node::Error(ErrorNode {
543            message: "Unclosed tag".into(),
544            raw_content: "<Notice>".into(),
545            position: span(1, 1, 0, 1, 9, 8),
546        });
547        let json = serde_json::to_value(&node).unwrap();
548        assert_eq!(json["type"], "error");
549        assert_eq!(json["message"], "Unclosed tag");
550        assert_eq!(json["rawContent"], "<Notice>");
551    }
552
553    #[test]
554    fn standard_block_omits_none_fields() {
555        let node = Node::Heading(StandardBlockNode {
556            depth: Some(2),
557            ordered: None,
558            checked: None,
559            id: None,
560            children: vec![],
561            position: span(1, 1, 0, 1, 10, 9),
562        });
563        let json = serde_json::to_value(&node).unwrap();
564        assert_eq!(json["depth"], 2);
565        assert!(json.get("ordered").is_none());
566        assert!(json.get("checked").is_none());
567        assert!(json.get("id").is_none());
568    }
569
570    #[test]
571    fn roundtrip_component_node() {
572        let original = Node::Component(ComponentNode {
573            name: "Chart".into(),
574            is_inline: true,
575            attributes: vec![],
576            children: vec![],
577            raw_content: String::new(),
578            position: span(1, 1, 0, 1, 10, 9),
579        });
580        let serialized = serde_json::to_string(&original).unwrap();
581        let deserialized: Node = serde_json::from_str(&serialized).unwrap();
582        assert_eq!(original, deserialized);
583    }
584}