Skip to main content

rd_ast/view/
figure.rs

1//! A borrowed view of `\figure` arguments.
2//!
3//! In an R-like context such as `\usage{\figure{f}{o}}`, the parser gives
4//! `\figure` only its first brace argument and the second brace remains RCode
5//! content of the parent. This view therefore sees a one-argument figure and
6//! MUST NOT absorb siblings as a second argument. Attribute parsing and image
7//! alt-text policy belong to consumers.
8
9use super::*;
10
11#[derive(Debug, Clone, PartialEq)]
12#[non_exhaustive]
13pub enum RdFigureSecondArgument<'a> {
14    AltText {
15        nodes: &'a [RdNode],
16        text: String,
17    },
18    Options {
19        nodes: &'a [RdNode],
20        attributes: String,
21    },
22}
23
24impl<'a> RdFigureSecondArgument<'a> {
25    pub fn nodes(&self) -> &'a [RdNode] {
26        match self {
27            Self::AltText { nodes, .. } | Self::Options { nodes, .. } => nodes,
28        }
29    }
30    pub fn alt_text(&self) -> Option<&str> {
31        match self {
32            Self::AltText { text, .. } => Some(text),
33            Self::Options { .. } => None,
34        }
35    }
36    pub fn option_attributes(&self) -> Option<&str> {
37        match self {
38            Self::AltText { .. } => None,
39            Self::Options { attributes, .. } => Some(attributes),
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct RdFigure<'a> {
46    path: RdPath,
47    file_nodes: &'a [RdNode],
48    file: String,
49    second: Option<RdFigureSecondArgument<'a>>,
50}
51
52impl<'a> RdFigure<'a> {
53    pub fn path(&self) -> &RdPath {
54        &self.path
55    }
56    pub fn file_nodes(&self) -> &'a [RdNode] {
57        self.file_nodes
58    }
59    pub fn file(&self) -> &str {
60        &self.file
61    }
62    pub fn second(&self) -> Option<&RdFigureSecondArgument<'a>> {
63        self.second.as_ref()
64    }
65}
66
67impl RdNode {
68    pub fn figure(&self, base_path: &RdPath) -> Option<RdFigure<'_>> {
69        self.inspect_figure(base_path).ok().flatten()
70    }
71    pub fn inspect_figure(&self, base_path: &RdPath) -> Result<Option<RdFigure<'_>>, RdShapeError> {
72        let tagged = match self {
73            RdNode::Tagged(tagged) => tagged,
74            RdNode::Raw(raw) => {
75                let Some(tag) = raw.tag().map(RdTag::from_rd_tag) else {
76                    return Ok(None);
77                };
78                if tag == RdTag::Figure {
79                    return Err(shape(
80                        base_path.clone(),
81                        Some(tag),
82                        RdShapeErrorKind::UnexpectedNode {
83                            expected: RdExpectedNode::Tagged,
84                            actual: RdNodeKind::Raw,
85                        },
86                    ));
87                }
88                return Ok(None);
89            }
90            _ => return Ok(None),
91        };
92        if tagged.tag() != &RdTag::Figure {
93            return Ok(None);
94        }
95        if tagged.option().is_some() {
96            return Err(shape(
97                base_path.clone(),
98                Some(RdTag::Figure),
99                RdShapeErrorKind::UnexpectedOption,
100            ));
101        }
102        let children = tagged.children();
103        if !(1..=2).contains(&children.len()) {
104            return Err(shape(
105                base_path.clone(),
106                Some(RdTag::Figure),
107                RdShapeErrorKind::WrongArity {
108                    expected: RdArity::Range { min: 1, max: 2 },
109                    actual: children.len(),
110                },
111            ));
112        }
113        let mut groups = Vec::with_capacity(children.len());
114        for (index, child) in children.iter().enumerate() {
115            let Some(group) = child.as_group() else {
116                return Err(shape(
117                    base_path.with_child(index),
118                    Some(RdTag::Figure),
119                    RdShapeErrorKind::UnexpectedNode {
120                        expected: RdExpectedNode::Group,
121                        actual: RdNodeKind::of(child),
122                    },
123                ));
124            };
125            groups.push(group.children());
126        }
127        let file_nodes = groups[0];
128        let file = concat_exact(
129            file_nodes,
130            RdNodeKind::Verb,
131            &base_path.with_child(0),
132            &RdTag::Figure,
133        )?;
134        let second = groups
135            .get(1)
136            .map(|nodes| {
137                let text = concat_exact(
138                    nodes,
139                    RdNodeKind::Verb,
140                    &base_path.with_child(1),
141                    &RdTag::Figure,
142                )?;
143                Ok(
144                    if let Some(attributes) = text
145                        .strip_prefix("options:")
146                        .filter(|rest| rest.chars().next().is_some_and(char::is_whitespace))
147                    {
148                        RdFigureSecondArgument::Options {
149                            nodes,
150                            attributes: attributes.trim_start().to_owned(),
151                        }
152                    } else {
153                        RdFigureSecondArgument::AltText { nodes, text }
154                    },
155                )
156            })
157            .transpose()?;
158        Ok(Some(RdFigure {
159            path: base_path.clone(),
160            file_nodes,
161            file,
162            second,
163        }))
164    }
165}