Skip to main content

rd_ast/
document.rs

1//! [`RdDocument`] and [`RdNode`]: the core Rd document tree.
2//!
3//! The normative node, ordering, group, and option rules are in the crate's
4//! included `CONTRACT.md`.
5
6use crate::{RawRdNode, RdTag};
7
8/// A complete parsed Rd document.
9///
10/// `nodes` are the top-level children of the parsed Rd object: a mix of
11/// section-tagged nodes (`\name`, `\title`, `\description`, ...) and the
12/// whitespace `TEXT` leaves R's parser leaves between them. Nothing is
13/// filtered out at this layer -- see the crate-level documentation for the
14/// losslessness rationale.
15#[derive(Debug, Clone, PartialEq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct RdDocument {
18    /// The document's top-level nodes, in source order.
19    nodes: Vec<RdNode>,
20}
21
22impl RdDocument {
23    pub fn new(nodes: Vec<RdNode>) -> Self {
24        Self { nodes }
25    }
26
27    pub fn nodes(&self) -> &[RdNode] {
28        &self.nodes
29    }
30
31    pub fn into_nodes(self) -> Vec<RdNode> {
32        self.nodes
33    }
34}
35
36impl From<Vec<RdNode>> for RdDocument {
37    fn from(nodes: Vec<RdNode>) -> Self {
38        Self::new(nodes)
39    }
40}
41
42impl IntoIterator for RdDocument {
43    type Item = RdNode;
44    type IntoIter = std::vec::IntoIter<RdNode>;
45
46    fn into_iter(self) -> Self::IntoIter {
47        self.nodes.into_iter()
48    }
49}
50
51impl<'a> IntoIterator for &'a RdDocument {
52    type Item = &'a RdNode;
53    type IntoIter = std::slice::Iter<'a, RdNode>;
54
55    fn into_iter(self) -> Self::IntoIter {
56        self.nodes.iter()
57    }
58}
59
60/// A tagged node with optional bracket content and positional children.
61#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct RdTagged {
64    tag: RdTag,
65    option: Option<Vec<RdNode>>,
66    children: Vec<RdNode>,
67}
68
69impl RdTagged {
70    pub fn new(tag: RdTag, option: Option<Vec<RdNode>>, children: Vec<RdNode>) -> Self {
71        Self {
72            tag,
73            option,
74            children,
75        }
76    }
77
78    pub fn tag(&self) -> &RdTag {
79        &self.tag
80    }
81
82    pub fn option(&self) -> Option<&[RdNode]> {
83        self.option.as_deref()
84    }
85
86    pub fn children(&self) -> &[RdNode] {
87        &self.children
88    }
89
90    pub fn into_parts(self) -> (RdTag, Option<Vec<RdNode>>, Vec<RdNode>) {
91        (self.tag, self.option, self.children)
92    }
93}
94
95/// An untagged positional-argument group, distinct from the `LIST` pseudo-tag.
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct RdGroup {
99    children: Vec<RdNode>,
100}
101
102impl RdGroup {
103    pub fn new(children: Vec<RdNode>) -> Self {
104        Self { children }
105    }
106
107    pub fn children(&self) -> &[RdNode] {
108        &self.children
109    }
110
111    pub fn into_children(self) -> Vec<RdNode> {
112        self.children
113    }
114}
115
116impl From<Vec<RdNode>> for RdGroup {
117    fn from(children: Vec<RdNode>) -> Self {
118        Self::new(children)
119    }
120}
121
122/// A single node of an Rd document tree.
123///
124/// The design is mostly generic: four leaf kinds carry raw text, one
125/// variant ([`Tagged`](RdNode::Tagged)) represents *every* known markup
126/// macro uniformly via [`RdTag`], and [`Raw`](RdNode::Raw) is the lossless
127/// fallback for anything that doesn't fit. See the crate-level
128/// documentation for why semantics (link targets, argument pairing, table
129/// rows) are intentionally left out of this enum.
130#[derive(Debug, Clone, PartialEq)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132#[non_exhaustive]
133pub enum RdNode {
134    /// A `TEXT`-tagged leaf: plain documentation text.
135    Text(String),
136    /// An `RCODE`-tagged leaf: R source code, as found inside `\usage`,
137    /// `\examples`, and similar code-valued sections/macros.
138    RCode(String),
139    /// A `VERB`-tagged leaf: verbatim text, as found inside `\verb{...}`
140    /// and `\preformatted{...}`.
141    Verb(String),
142    /// A `COMMENT`-tagged leaf: a `%`-introduced Rd comment, preserved
143    /// (including the leading `%`) rather than dropped, since
144    /// `parse_Rd(keep.source = TRUE)` keeps them and a lossless AST must
145    /// be able to represent whatever the producer handed it.
146    Comment(String),
147    /// Any node carrying a known or unknown macro-style `Rd_tag`.
148    Tagged(RdTagged),
149    /// An untagged positional-argument group.
150    Group(RdGroup),
151    /// A lossless fallback for a node that doesn't fit the shapes above:
152    /// genuinely non-canonical structures such as nodes with unexpected
153    /// attributes. Positional-argument groups use [`RdNode::Group`].
154    Raw(RawRdNode),
155}
156
157impl RdNode {
158    pub fn tagged(tag: RdTag, option: Option<Vec<RdNode>>, children: Vec<RdNode>) -> Self {
159        Self::Tagged(RdTagged::new(tag, option, children))
160    }
161
162    pub fn group(children: Vec<RdNode>) -> Self {
163        Self::Group(RdGroup::new(children))
164    }
165
166    pub fn as_tagged(&self) -> Option<&RdTagged> {
167        match self {
168            Self::Tagged(node) => Some(node),
169            _ => None,
170        }
171    }
172    pub fn as_group(&self) -> Option<&RdGroup> {
173        match self {
174            Self::Group(node) => Some(node),
175            _ => None,
176        }
177    }
178    pub fn as_raw(&self) -> Option<&RawRdNode> {
179        match self {
180            Self::Raw(node) => Some(node),
181            _ => None,
182        }
183    }
184
185    pub fn into_tagged(self) -> Result<RdTagged, Self> {
186        match self {
187            Self::Tagged(node) => Ok(node),
188            other => Err(other),
189        }
190    }
191    pub fn into_group(self) -> Result<RdGroup, Self> {
192        match self {
193            Self::Group(node) => Ok(node),
194            other => Err(other),
195        }
196    }
197    pub fn into_raw(self) -> Result<RawRdNode, Self> {
198        match self {
199            Self::Raw(node) => Ok(node),
200            other => Err(other),
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{RawRdValue, producer};
209
210    /// A minimal but representative document: `\name`, `\title`, and a
211    /// `\description` containing inline markup, separated by the
212    /// whitespace `TEXT` leaves a real parse produces.
213    fn sample_document() -> RdDocument {
214        RdDocument::new(vec![
215            RdNode::tagged(RdTag::Name, None, vec![RdNode::Text("example".to_string())]),
216            RdNode::Text("\n".to_string()),
217            RdNode::tagged(
218                RdTag::Title,
219                None,
220                vec![RdNode::Text("An example topic".to_string())],
221            ),
222            RdNode::Text("\n".to_string()),
223            RdNode::tagged(
224                RdTag::Description,
225                None,
226                vec![
227                    RdNode::Text("See ".to_string()),
228                    RdNode::tagged(
229                        RdTag::Link,
230                        Some(vec![RdNode::Text("base".to_string())]),
231                        vec![RdNode::Text("print".to_string())],
232                    ),
233                    RdNode::Text(" for details.".to_string()),
234                ],
235            ),
236        ])
237    }
238
239    #[test]
240    fn builds_and_matches_expected_shape() {
241        let doc = sample_document();
242        assert_eq!(doc.nodes().len(), 5);
243        let Some(tagged) = doc.nodes()[0].as_tagged() else {
244            panic!("expected a Tagged node");
245        };
246        assert_eq!(tagged.tag(), &RdTag::Name);
247        assert_eq!(tagged.children(), &[RdNode::Text("example".to_string())]);
248    }
249
250    #[test]
251    fn tagged_option_carries_markup() {
252        let doc = sample_document();
253        let Some(description) = doc.nodes()[4].as_tagged() else {
254            panic!("expected the description's Tagged node");
255        };
256        let Some(link) = description.children()[1].as_tagged() else {
257            panic!(r"expected the \link node");
258        };
259        assert_eq!(link.tag(), &RdTag::Link);
260        assert_eq!(link.option(), Some(&[RdNode::Text("base".to_string())][..]));
261    }
262
263    /// [`RdNode::Raw`] must preserve an untagged list with an unexpected
264    /// attribute, without losing any of it.
265    #[test]
266    fn raw_node_preserves_untagged_group_and_attributes() {
267        let raw = producer::raw_node(
268            None,
269            None,
270            vec![RdNode::Text("print".to_string())],
271            None,
272            vec![producer::raw_attribute(
273                "class".to_string(),
274                producer::raw_object(
275                    RawRdValue::Character(vec![Some("weird".to_string())]),
276                    Vec::new(),
277                ),
278            )],
279        );
280        let node = RdNode::Raw(raw.clone());
281        let RdNode::Raw(inner) = &node else {
282            panic!("expected Raw");
283        };
284        assert_eq!(inner, &raw);
285        assert_eq!(inner.tag(), None);
286        assert_eq!(inner.attributes().len(), 1);
287    }
288
289    #[test]
290    fn equal_documents_compare_equal() {
291        assert_eq!(sample_document(), sample_document());
292    }
293}