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#[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 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}