1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4#[non_exhaustive]
5pub enum RdLifecycleStage {
6 Experimental,
7 Stable,
8 Superseded,
9 Deprecated,
10 Maturing,
11 Questioning,
12 SoftDeprecated,
13 Defunct,
14 Retired,
15 Unknown(String),
16}
17
18impl RdLifecycleStage {
19 pub fn as_str(&self) -> &str {
20 match self {
21 Self::Experimental => "experimental",
22 Self::Stable => "stable",
23 Self::Superseded => "superseded",
24 Self::Deprecated => "deprecated",
25 Self::Maturing => "maturing",
26 Self::Questioning => "questioning",
27 Self::SoftDeprecated => "soft-deprecated",
28 Self::Defunct => "defunct",
29 Self::Retired => "retired",
30 Self::Unknown(value) => value,
31 }
32 }
33
34 pub fn is_current(&self) -> bool {
35 matches!(
36 self,
37 Self::Experimental | Self::Stable | Self::Superseded | Self::Deprecated
38 )
39 }
40
41 pub fn is_legacy(&self) -> bool {
42 matches!(
43 self,
44 Self::Maturing
45 | Self::Questioning
46 | Self::SoftDeprecated
47 | Self::Defunct
48 | Self::Retired
49 )
50 }
51
52 pub fn is_known(&self) -> bool {
53 !matches!(self, Self::Unknown(_))
54 }
55}
56
57fn stage_from_figure(figure: &RdFigure<'_>) -> Option<RdLifecycleStage> {
58 let basename = figure.file().rsplit('/').next()?;
59 if !basename.starts_with("lifecycle-") {
60 return None;
61 }
62 let stem = basename
63 .rfind('.')
64 .map_or(basename, |index| &basename[..index]);
65 let spelling = stem.strip_prefix("lifecycle-")?;
66 if spelling.is_empty() {
67 return None;
68 }
69 Some(match spelling {
70 value if value.eq_ignore_ascii_case("experimental") => RdLifecycleStage::Experimental,
71 value if value.eq_ignore_ascii_case("stable") => RdLifecycleStage::Stable,
72 value if value.eq_ignore_ascii_case("superseded") => RdLifecycleStage::Superseded,
73 value if value.eq_ignore_ascii_case("deprecated") => RdLifecycleStage::Deprecated,
74 value if value.eq_ignore_ascii_case("maturing") => RdLifecycleStage::Maturing,
75 value if value.eq_ignore_ascii_case("questioning") => RdLifecycleStage::Questioning,
76 value if value.eq_ignore_ascii_case("soft-deprecated") => RdLifecycleStage::SoftDeprecated,
77 value if value.eq_ignore_ascii_case("defunct") => RdLifecycleStage::Defunct,
78 value if value.eq_ignore_ascii_case("retired") => RdLifecycleStage::Retired,
79 value => RdLifecycleStage::Unknown(value.to_owned()),
80 })
81}
82
83#[derive(Debug, Clone, PartialEq)]
84pub struct RdLifecycleBadgeShape<'a> {
85 conditional: RdConditional<'a>,
86 href: RdHref<'a>,
87 fallback: RdInlineSpan<'a>,
88 fallback_text: String,
89}
90
91impl<'a> RdLifecycleBadgeShape<'a> {
92 pub fn conditional(&self) -> &RdConditional<'a> {
93 &self.conditional
94 }
95 pub fn href(&self) -> &RdHref<'a> {
96 &self.href
97 }
98 pub fn fallback(&self) -> &RdInlineSpan<'a> {
99 &self.fallback
100 }
101 pub fn fallback_text(&self) -> &str {
102 &self.fallback_text
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct RdLifecycleBadge<'a> {
108 stage: RdLifecycleStage,
109 figure: RdFigure<'a>,
110 canonical_shape: Option<RdLifecycleBadgeShape<'a>>,
111}
112
113impl<'a> RdLifecycleBadge<'a> {
114 pub fn stage(&self) -> &RdLifecycleStage {
115 &self.stage
116 }
117 pub fn figure(&self) -> &RdFigure<'a> {
118 &self.figure
119 }
120 pub fn path(&self) -> &RdPath {
121 self.figure.path()
122 }
123 pub fn canonical_shape(&self) -> Option<&RdLifecycleBadgeShape<'a>> {
124 self.canonical_shape.as_ref()
125 }
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct RdLifecycleBadges<'a> {
130 badges: Vec<RdLifecycleBadge<'a>>,
131 diagnostics: Vec<RdShapeError>,
132}
133
134impl<'a> RdLifecycleBadges<'a> {
135 pub fn as_slice(&self) -> &[RdLifecycleBadge<'a>] {
136 &self.badges
137 }
138 pub fn iter(&self) -> impl Iterator<Item = &RdLifecycleBadge<'a>> {
139 self.badges.iter()
140 }
141 pub fn first(&self) -> Option<&RdLifecycleBadge<'a>> {
142 self.badges.first()
143 }
144 pub fn diagnostics(&self) -> &[RdShapeError] {
145 &self.diagnostics
146 }
147}
148
149impl<'a> IntoIterator for &'a RdLifecycleBadges<'a> {
150 type Item = &'a RdLifecycleBadge<'a>;
151 type IntoIter = std::slice::Iter<'a, RdLifecycleBadge<'a>>;
152 fn into_iter(self) -> Self::IntoIter {
153 self.badges.iter()
154 }
155}
156
157fn significant(nodes: &[RdNode]) -> impl Iterator<Item = (usize, &RdNode)> {
158 nodes
159 .iter()
160 .enumerate()
161 .filter(|(_, node)| !is_inter_item_trivia(node))
162}
163
164fn canonical_match<'a>(
165 node: &'a RdNode,
166 path: &RdPath,
167) -> Option<(
168 RdLifecycleBadgeShape<'a>,
169 RdFigure<'a>,
170 RdLifecycleStage,
171 RdPath,
172)> {
173 let conditional = node.inspect_conditional(path).ok().flatten()?;
174 if conditional.kind() != RdConditionalKind::IfElse || conditional.format() != "html" {
175 return None;
176 }
177 let (then_index, then_node) = exactly_one(conditional.then_branch())?;
178 let href_tagged = then_node.as_tagged()?;
179 let href_path = path.with_child(1).with_child(then_index);
180 let href = href_tagged.inspect_href(&href_path).ok()?;
181 let (url_index, url_node) = exactly_one(href.url())?;
182 if !matches!(url_node, RdNode::Verb(value) if !value.is_empty()) {
183 return None;
184 }
185 let (display_index, display_node) = exactly_one(href.display())?;
186 let figure_path = href_path.with_child(1).with_child(display_index);
187 let figure = display_node.inspect_figure(&figure_path).ok().flatten()?;
188 let stage = stage_from_figure(&figure)?;
189 let (fallback_index, fallback_node) = exactly_one(conditional.else_branch()?)?;
190 let fallback_path = path.with_child(2).with_child(fallback_index);
191 let fallback = fallback_node.inline_span(&fallback_path)?;
192 if fallback.kind() != RdInlineSpanKind::Strong {
193 return None;
194 }
195 let [RdNode::Text(text)] = fallback.body() else {
196 return None;
197 };
198 let inner = text.strip_prefix('[')?.strip_suffix(']')?;
199 if inner.is_empty() || normalize_stage(inner) != normalize_stage(stage.as_str()) {
200 return None;
201 }
202 let _ = url_index;
203 Some((
204 RdLifecycleBadgeShape {
205 conditional,
206 href,
207 fallback,
208 fallback_text: inner.to_owned(),
209 },
210 figure,
211 stage,
212 figure_path,
213 ))
214}
215
216fn exactly_one(nodes: &[RdNode]) -> Option<(usize, &RdNode)> {
217 let mut items = significant(nodes);
218 let item = items.next()?;
219 items.next().is_none().then_some(item)
220}
221
222fn normalize_stage(value: &str) -> String {
223 value
224 .split_whitespace()
225 .collect::<Vec<_>>()
226 .join("-")
227 .to_ascii_lowercase()
228}
229
230struct Collector<'a> {
231 badges: Vec<RdLifecycleBadge<'a>>,
232 diagnostics: Vec<RdShapeError>,
233 inspect: bool,
234}
235
236impl<'a> Collector<'a> {
237 fn visit(&mut self, nodes: &'a [RdNode], base: &RdPath, skip: Option<&RdPath>) {
238 for (index, node) in nodes.iter().enumerate() {
239 let path = base.with_child(index);
240 let mut nested_skip = None;
241 if let Some((shape, figure, stage, figure_path)) = canonical_match(node, &path) {
242 self.badges.push(RdLifecycleBadge {
243 stage,
244 figure,
245 canonical_shape: Some(shape),
246 });
247 nested_skip = Some(figure_path);
248 }
249 if skip != Some(&path) {
250 match node.inspect_figure(&path) {
251 Ok(Some(figure)) => {
252 if let Some(stage) = stage_from_figure(&figure) {
253 self.badges.push(RdLifecycleBadge {
254 stage,
255 figure,
256 canonical_shape: None,
257 });
258 }
259 }
260 Ok(None) => {}
261 Err(error) => self.report(error),
262 }
263 }
264 if matches!(node, RdNode::Raw(_)) {
265 continue;
266 }
267 match node {
268 RdNode::Tagged(tagged) => {
269 self.visit(tagged.children(), &path, nested_skip.as_ref().or(skip))
270 }
271 RdNode::Group(group) => {
272 self.visit(group.children(), &path, nested_skip.as_ref().or(skip))
273 }
274 _ => {}
275 }
276 }
277 }
278
279 fn report(&mut self, error: RdShapeError) {
280 if self.inspect {
281 self.diagnostics.push(error);
282 }
283 }
284}
285
286impl RdDocument {
287 pub fn lifecycle_badges(&self) -> RdLifecycleBadges<'_> {
288 self.collect_lifecycle(false)
289 }
290 pub fn inspect_lifecycle_badges(&self) -> RdLifecycleBadges<'_> {
291 self.collect_lifecycle(true)
292 }
293
294 fn collect_lifecycle(&self, inspect: bool) -> RdLifecycleBadges<'_> {
295 let mut collector = Collector {
296 badges: Vec::new(),
297 diagnostics: Vec::new(),
298 inspect,
299 };
300 let mut first_description = None;
301 for (index, node) in self.nodes().iter().enumerate() {
302 let path = top_path(index);
303 let Some(tagged) = node.as_tagged() else {
304 if node
305 .as_raw()
306 .and_then(|raw| raw.tag())
307 .is_some_and(|tag| tag == RdTag::Description.as_rd_tag())
308 {
309 collector.report(shape(
310 path.clone(),
311 Some(RdTag::Description),
312 RdShapeErrorKind::UnexpectedNode {
313 expected: RdExpectedNode::Tagged,
314 actual: RdNodeKind::Raw,
315 },
316 ));
317 }
318 continue;
319 };
320 if tagged.tag() != &RdTag::Description {
321 continue;
322 }
323 if let Some(first) = first_description {
324 collector.report(shape(
325 path.clone(),
326 Some(RdTag::Description),
327 RdShapeErrorKind::Duplicate {
328 construct: RdConstruct::Tag(RdTag::Description),
329 first_path: top_path(first),
330 },
331 ));
332 } else {
333 first_description = Some(index);
334 }
335 if tagged.option().is_some() {
336 collector.report(shape(
337 path.clone(),
338 Some(RdTag::Description),
339 RdShapeErrorKind::UnexpectedOption,
340 ));
341 }
342 collector.visit(tagged.children(), &path, None);
343 }
344 RdLifecycleBadges {
345 badges: collector.badges,
346 diagnostics: collector.diagnostics,
347 }
348 }
349}