Skip to main content

rd_ast/view/
link.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq)]
4pub struct RdLink<'a> {
5    path: RdPath,
6    display: &'a [RdNode],
7    destination: RdLinkDestination<'a>,
8}
9
10impl<'a> RdLink<'a> {
11    pub fn path(&self) -> &RdPath {
12        &self.path
13    }
14    pub fn display(&self) -> &'a [RdNode] {
15        self.display
16    }
17    pub fn destination(&self) -> &RdLinkDestination<'a> {
18        &self.destination
19    }
20}
21
22#[derive(Debug, Clone, PartialEq)]
23#[non_exhaustive]
24pub enum RdLinkDestination<'a> {
25    DisplayText {
26        nodes: &'a [RdNode],
27    },
28    Explicit {
29        topic: &'a str,
30    },
31    Package {
32        package: &'a str,
33        topic: RdLinkTopic<'a>,
34    },
35}
36
37#[derive(Debug, Clone, PartialEq)]
38#[non_exhaustive]
39pub enum RdLinkTopic<'a> {
40    DisplayText(&'a [RdNode]),
41    Explicit(&'a str),
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct RdHref<'a> {
46    path: RdPath,
47    url: &'a [RdNode],
48    display: &'a [RdNode],
49}
50impl<'a> RdHref<'a> {
51    pub fn path(&self) -> &RdPath {
52        &self.path
53    }
54    pub fn url(&self) -> &'a [RdNode] {
55        self.url
56    }
57    pub fn display(&self) -> &'a [RdNode] {
58        self.display
59    }
60}
61
62/// A source-level `\linkS4class` view. Link resolution and rendering policy
63/// belongs to consumers, and differs from [`RdLink`].
64#[derive(Debug, Clone, PartialEq)]
65pub struct RdS4ClassLink<'a> {
66    path: RdPath,
67    class: &'a [RdNode],
68    package: Option<&'a [RdNode]>,
69}
70
71impl<'a> RdS4ClassLink<'a> {
72    pub fn path(&self) -> &RdPath {
73        &self.path
74    }
75    pub fn class(&self) -> &'a [RdNode] {
76        self.class
77    }
78    pub fn package(&self) -> Option<&'a [RdNode]> {
79        self.package
80    }
81    pub fn class_text(&self) -> Option<String> {
82        text_only(self.class)
83    }
84    pub fn package_text(&self) -> Option<String> {
85        self.package.and_then(text_only)
86    }
87}
88
89fn text_only(nodes: &[RdNode]) -> Option<String> {
90    let mut result = String::new();
91    for node in nodes {
92        let RdNode::Text(value) = node else {
93            return None;
94        };
95        result.push_str(value);
96    }
97    Some(result)
98}
99
100impl RdNode {
101    pub fn s4_class_link(&self, base_path: &RdPath) -> Option<RdS4ClassLink<'_>> {
102        self.inspect_s4_class_link(base_path).ok().flatten()
103    }
104    pub fn inspect_s4_class_link(
105        &self,
106        base_path: &RdPath,
107    ) -> Result<Option<RdS4ClassLink<'_>>, RdShapeError> {
108        let tagged = match self {
109            RdNode::Tagged(tagged) => tagged,
110            RdNode::Raw(raw) => {
111                let Some(tag) = raw.tag().map(RdTag::from_rd_tag) else {
112                    return Ok(None);
113                };
114                if tag == RdTag::LinkS4Class {
115                    return Err(shape(
116                        base_path.clone(),
117                        Some(tag),
118                        RdShapeErrorKind::UnexpectedNode {
119                            expected: RdExpectedNode::Tagged,
120                            actual: RdNodeKind::Raw,
121                        },
122                    ));
123                }
124                return Ok(None);
125            }
126            _ => return Ok(None),
127        };
128        if tagged.tag() != &RdTag::LinkS4Class {
129            return Ok(None);
130        }
131        Ok(Some(RdS4ClassLink {
132            path: base_path.clone(),
133            class: tagged.children(),
134            package: tagged.option(),
135        }))
136    }
137}
138
139impl RdTagged {
140    /// A wrong tag uses `UnexpectedNode`; `actual: Tagged` identifies the
141    /// node kind while the expected variant identifies the requested view.
142    pub fn inspect_link<'a>(&'a self, base_path: &RdPath) -> Result<RdLink<'a>, RdShapeError> {
143        if self.tag() != &RdTag::Link {
144            return Err(shape(
145                base_path.clone(),
146                Some(self.tag().clone()),
147                RdShapeErrorKind::UnexpectedNode {
148                    expected: RdExpectedNode::Link,
149                    actual: RdNodeKind::Tagged,
150                },
151            ));
152        }
153        let display = self.children();
154        let all_text = |nodes: &[RdNode], path: &RdPath| -> Result<(), RdShapeError> {
155            for (index, node) in nodes.iter().enumerate() {
156                if !matches!(node, RdNode::Text(_)) {
157                    return Err(shape(
158                        path.with_child(index),
159                        Some(RdTag::Link),
160                        RdShapeErrorKind::UnexpectedContent {
161                            actual: RdNodeKind::of(node),
162                        },
163                    ));
164                }
165            }
166            Ok(())
167        };
168        let destination = match self.option() {
169            None => {
170                all_text(display, base_path)?;
171                RdLinkDestination::DisplayText { nodes: display }
172            }
173            Some(option) => {
174                let option_path = base_path.with_option();
175                let [node] = option else {
176                    return Err(shape(
177                        option_path,
178                        Some(RdTag::Link),
179                        RdShapeErrorKind::WrongArity {
180                            expected: RdArity::Exactly(1),
181                            actual: option.len(),
182                        },
183                    ));
184                };
185                let RdNode::Text(value) = node else {
186                    return Err(shape(
187                        option_path.with_child(0),
188                        Some(RdTag::Link),
189                        RdShapeErrorKind::UnexpectedContent {
190                            actual: RdNodeKind::of(node),
191                        },
192                    ));
193                };
194                let value = value.as_str();
195                if let Some(topic) = value.strip_prefix('=') {
196                    RdLinkDestination::Explicit { topic }
197                } else if let Some((package, topic)) = value.split_once(':') {
198                    RdLinkDestination::Package {
199                        package,
200                        topic: RdLinkTopic::Explicit(topic),
201                    }
202                } else {
203                    all_text(display, base_path)?;
204                    RdLinkDestination::Package {
205                        package: value,
206                        topic: RdLinkTopic::DisplayText(display),
207                    }
208                }
209            }
210        };
211        Ok(RdLink {
212            path: base_path.clone(),
213            display,
214            destination,
215        })
216    }
217
218    pub fn inspect_href<'a>(&'a self, base_path: &RdPath) -> Result<RdHref<'a>, RdShapeError> {
219        if self.tag() != &RdTag::Href {
220            return Err(shape(
221                base_path.clone(),
222                Some(self.tag().clone()),
223                RdShapeErrorKind::UnexpectedNode {
224                    expected: RdExpectedNode::Href,
225                    actual: RdNodeKind::Tagged,
226                },
227            ));
228        }
229        if self.option().is_some() {
230            return Err(shape(
231                base_path.clone(),
232                Some(RdTag::Href),
233                RdShapeErrorKind::UnexpectedOption,
234            ));
235        }
236        let children = self.children();
237        if children.len() != 2 {
238            return Err(shape(
239                base_path.clone(),
240                Some(RdTag::Href),
241                RdShapeErrorKind::WrongArity {
242                    expected: RdArity::Exactly(2),
243                    actual: children.len(),
244                },
245            ));
246        }
247        let [url, display] = children else {
248            unreachable!()
249        };
250        let url = url.as_group().ok_or_else(|| {
251            shape(
252                base_path.with_child(0),
253                Some(RdTag::Href),
254                RdShapeErrorKind::UnexpectedNode {
255                    expected: RdExpectedNode::Group,
256                    actual: RdNodeKind::of(url),
257                },
258            )
259        })?;
260        let display = display.as_group().ok_or_else(|| {
261            shape(
262                base_path.with_child(1),
263                Some(RdTag::Href),
264                RdShapeErrorKind::UnexpectedNode {
265                    expected: RdExpectedNode::Group,
266                    actual: RdNodeKind::of(display),
267                },
268            )
269        })?;
270        Ok(RdHref {
271            path: base_path.clone(),
272            url: url.children(),
273            display: display.children(),
274        })
275    }
276}