Skip to main content

rd_ast/view/
lifecycle.rs

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}