Skip to main content

rd_ast/view/
document.rs

1use super::list::inspect_two_group_item;
2use super::*;
3
4/// A custom `\section{title}{body}` node (see [`RdDocument::sections`]).
5///
6/// Not the standard, fixed-vocabulary sections (`\description`, `\value`,
7/// ...) -- those are read individually via
8/// [`RdDocument::title`]/[`RdDocument::description`]/etc.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct RdSection<'a> {
11    /// The section's title, i.e. `\section{title}{...}`'s first argument
12    /// group.
13    pub title: &'a [RdNode],
14    /// The section's body, i.e. `\section{...}{body}`'s second argument
15    /// group.
16    pub body: &'a [RdNode],
17}
18
19/// A single `\item{name}{description}` entry within `\arguments` (see
20/// [`RdDocument::arguments`]).
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct RdArgument<'a> {
23    /// The argument's name, i.e. `\item{name}{...}`'s first argument
24    /// group.
25    pub name: &'a [RdNode],
26    /// The argument's description, i.e. `\item{...}{description}`'s
27    /// second argument group.
28    pub description: &'a [RdNode],
29}
30
31/// A borrowed, structurally valid `\alias{...}` view.
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct RdAlias<'a> {
34    nodes: &'a [RdNode],
35}
36
37/// A borrowed, structurally valid `\keyword{...}` view.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct RdKeyword<'a> {
40    nodes: &'a [RdNode],
41}
42
43impl<'a> RdKeyword<'a> {
44    pub fn nodes(&self) -> &'a [RdNode] {
45        self.nodes
46    }
47    pub fn text_contents(&self) -> String {
48        text_contents(self.nodes)
49    }
50}
51
52/// A borrowed, structurally valid `\concept{...}` view.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct RdConcept<'a> {
55    nodes: &'a [RdNode],
56}
57
58impl<'a> RdConcept<'a> {
59    pub fn nodes(&self) -> &'a [RdNode] {
60        self.nodes
61    }
62    pub fn text_contents(&self) -> String {
63        text_contents(self.nodes)
64    }
65}
66
67/// The kind of custom section-family node visited by [`RdDocument::section_tree`].
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum RdSectionKind {
71    Section,
72    Subsection,
73}
74
75/// A structurally valid custom section-family node in a document.
76#[derive(Debug, Clone, PartialEq)]
77pub struct RdSectionVisit<'a> {
78    path: RdPath,
79    kind: RdSectionKind,
80    nesting: usize,
81    title: &'a [RdNode],
82    body: &'a [RdNode],
83}
84
85impl<'a> RdSectionVisit<'a> {
86    pub fn path(&self) -> &RdPath {
87        &self.path
88    }
89    pub fn kind(&self) -> RdSectionKind {
90        self.kind
91    }
92    /// Returns syntactic nesting only; heading-level and rendering policy
93    /// belong to consumers.
94    pub fn nesting(&self) -> usize {
95        self.nesting
96    }
97    pub fn title(&self) -> &'a [RdNode] {
98        self.title
99    }
100    pub fn body(&self) -> &'a [RdNode] {
101        self.body
102    }
103}
104
105impl<'a> RdAlias<'a> {
106    pub fn nodes(&self) -> &'a [RdNode] {
107        self.nodes
108    }
109    pub fn text_contents(&self) -> String {
110        text_contents(self.nodes)
111    }
112}
113impl RdDocument {
114    /// Lossy: returns the first top-level `\title{...}`'s children, if any.
115    /// See [`Self::inspect_title`] for diagnostics.
116    ///
117    /// "First-wins": if `\title` appears more than once at the top level
118    /// (malformed input), only the first occurrence is returned. Only
119    /// [`RdNode::Tagged`] top-level nodes are considered -- see the
120    /// module-level documentation for why a top-level [`RdNode::Raw`] node
121    /// whose tag string happens to be `"\\title"` is not matched.
122    pub fn title(&self) -> Option<&[RdNode]> {
123        self.first_tagged_children(RdTag::Title)
124    }
125
126    /// Lossy: returns the first top-level `\description{...}`'s children, if any.
127    /// See [`Self::inspect_description`] for diagnostics.
128    ///
129    /// Same first-wins-on-duplicates and `Tagged`-only semantics as
130    /// [`RdDocument::title`].
131    pub fn description(&self) -> Option<&[RdNode]> {
132        self.first_tagged_children(RdTag::Description)
133    }
134
135    /// Lossy: returns the first top-level `\usage{...}`'s children, if any.
136    /// See [`Self::inspect_usage`] for diagnostics.
137    ///
138    /// Same first-wins-on-duplicates and `Tagged`-only semantics as
139    /// [`RdDocument::title`].
140    pub fn usage(&self) -> Option<&[RdNode]> {
141        self.first_tagged_children(RdTag::Usage)
142    }
143
144    /// Lossy: returns the first top-level `\value{...}`'s children, if any.
145    /// See [`Self::inspect_value`] for diagnostics.
146    ///
147    /// Same first-wins-on-duplicates and `Tagged`-only semantics as
148    /// [`RdDocument::title`].
149    pub fn value(&self) -> Option<&[RdNode]> {
150        self.first_tagged_children(RdTag::Value)
151    }
152
153    /// Lossy: returns the first top-level `\name{...}`'s children, if any.
154    /// See [`Self::inspect_name`] for diagnostics. First-wins on duplicates.
155    pub fn name(&self) -> Option<&[RdNode]> {
156        self.first_tagged_children(RdTag::Name)
157    }
158    /// Lossy: returns the first top-level `\details{...}`'s children, if any.
159    /// See [`Self::inspect_details`] for diagnostics. First-wins on duplicates.
160    pub fn details(&self) -> Option<&[RdNode]> {
161        self.first_tagged_children(RdTag::Details)
162    }
163    /// Lossy: returns the first top-level `\note{...}`'s children, if any.
164    /// See [`Self::inspect_note`] for diagnostics. First-wins on duplicates.
165    pub fn note(&self) -> Option<&[RdNode]> {
166        self.first_tagged_children(RdTag::Note)
167    }
168    /// Lossy: returns the first top-level `\author{...}`'s children, if any.
169    /// See [`Self::inspect_author`] for diagnostics. First-wins on duplicates.
170    pub fn author(&self) -> Option<&[RdNode]> {
171        self.first_tagged_children(RdTag::Author)
172    }
173    /// Lossy: returns the first top-level `\references{...}`'s children, if any.
174    /// See [`Self::inspect_references`] for diagnostics. First-wins on duplicates.
175    pub fn references(&self) -> Option<&[RdNode]> {
176        self.first_tagged_children(RdTag::References)
177    }
178    /// Lossy: returns the first top-level `\seealso{...}`'s children, if any.
179    /// See [`Self::inspect_see_also`] for diagnostics. First-wins on duplicates.
180    pub fn see_also(&self) -> Option<&[RdNode]> {
181        self.first_tagged_children(RdTag::SeeAlso)
182    }
183    /// Lossy: returns the first top-level `\examples{...}`'s children, if any.
184    /// See [`Self::inspect_examples`] for diagnostics. First-wins on duplicates.
185    pub fn examples(&self) -> Option<&[RdNode]> {
186        self.first_tagged_children(RdTag::Examples)
187    }
188    /// Lossy: returns the first top-level `\format{...}`'s children, if any.
189    /// See [`Self::inspect_format`] for diagnostics. First-wins on duplicates.
190    pub fn format(&self) -> Option<&[RdNode]> {
191        self.first_tagged_children(RdTag::Format)
192    }
193    /// Lossy: returns the first top-level `\source{...}`'s children, if any.
194    /// See [`Self::inspect_source`] for diagnostics. First-wins on duplicates.
195    pub fn source(&self) -> Option<&[RdNode]> {
196        self.first_tagged_children(RdTag::Source)
197    }
198    /// Lossy: returns the first top-level `\encoding{...}`'s children, if any.
199    /// See [`Self::inspect_encoding`] for diagnostics. First-wins on duplicates.
200    pub fn encoding(&self) -> Option<&[RdNode]> {
201        self.first_tagged_children(RdTag::Encoding)
202    }
203    /// Lossy: returns the first top-level `\docType{...}`'s children, if any.
204    /// See [`Self::inspect_doc_type`] for diagnostics. First-wins on duplicates.
205    pub fn doc_type(&self) -> Option<&[RdNode]> {
206        self.first_tagged_children(RdTag::DocType)
207    }
208    /// Lossy: returns the first top-level `\Rdversion{...}`'s children, if any.
209    /// See [`Self::inspect_rd_version`] for diagnostics. First-wins on duplicates.
210    pub fn rd_version(&self) -> Option<&[RdNode]> {
211        self.first_tagged_children(RdTag::Rdversion)
212    }
213    /// Lossy: returns the first top-level `\synopsis{...}`'s children, if any.
214    /// See [`Self::inspect_synopsis`] for diagnostics. First-wins on duplicates.
215    pub fn synopsis(&self) -> Option<&[RdNode]> {
216        self.first_tagged_children(RdTag::Synopsis)
217    }
218
219    /// The children of the first top-level `Tagged` node with the given
220    /// `tag`, if any.
221    fn first_tagged_children(&self, tag: RdTag) -> Option<&[RdNode]> {
222        self.nodes().iter().find_map(|node| {
223            let tagged = node.as_tagged()?;
224            (tagged.tag() == &tag).then(|| tagged.children())
225        })
226    }
227
228    /// Lossy: iterates the current topic's `\alias{...}` declarations, in source
229    /// order, yielding each alias's [`text_contents`] (no splitting, no
230    /// trimming -- an alias with empty children yields an empty string).
231    ///
232    /// This reads the topic's *own* `\alias` markup, i.e. the alias names
233    /// this specific Rd document declares for itself. It is unrelated to a
234    /// package-wide `help/aliases.rds` index (as read by e.g.
235    /// `rd-helpdb`'s `PackageHelpDb::aliases`/`resolve_alias`), which maps
236    /// every alias in a package to the topic file that documents it.
237    ///
238    /// Only top-level [`RdNode::Tagged`] nodes are considered -- see the
239    /// module-level documentation for why a top-level [`RdNode::Raw`] node
240    /// whose tag string happens to be `"\\alias"` is not matched.
241    pub fn aliases(&self) -> impl Iterator<Item = String> + '_ {
242        self.nodes().iter().filter_map(|node| {
243            let tagged = node.as_tagged()?;
244            (tagged.tag() == &RdTag::Alias).then(|| text_contents(tagged.children()))
245        })
246    }
247
248    /// Lossy: iterates top-level `\keyword{...}` declarations in source order.
249    pub fn keywords(&self) -> impl Iterator<Item = String> + '_ {
250        self.nodes().iter().filter_map(|node| {
251            let tagged = node.as_tagged()?;
252            (tagged.tag() == &RdTag::Keyword).then(|| text_contents(tagged.children()))
253        })
254    }
255
256    /// Lossy: iterates top-level `\concept{...}` declarations in source order.
257    pub fn concepts(&self) -> impl Iterator<Item = String> + '_ {
258        self.nodes().iter().filter_map(|node| {
259            let tagged = node.as_tagged()?;
260            (tagged.tag() == &RdTag::Concept).then(|| text_contents(tagged.children()))
261        })
262    }
263
264    /// Lossy: iterates the document's custom `\section{title}{body}` nodes, in
265    /// source order.
266    ///
267    /// This is distinct from the standard, fixed-vocabulary sections
268    /// (`\description`, `\value`, `\details`, ...), which are read
269    /// individually (see [`RdDocument::title`], [`RdDocument::description`],
270    /// etc.). It only recognizes custom `\section{...}{...}` markup
271    /// (`RdTag::Section`).
272    ///
273    /// A top-level node is recognized as a section only when it is a
274    /// `Tagged` node with `RdTag::Section`, no option, and
275    /// exactly two `RdNode::Group` children (the title/body positional-
276    /// argument groups the `\section` macro lowers to). Anything else is
277    /// silently skipped rather than erroring.
278    /// For nested `\subsection` traversal, see [`Self::section_tree`].
279    pub fn sections(&self) -> impl Iterator<Item = RdSection<'_>> {
280        self.nodes().iter().filter_map(|node| {
281            let tagged = node.as_tagged()?;
282            if tagged.tag() != &RdTag::Section || tagged.option().is_some() {
283                return None;
284            }
285            let [RdNode::Group(title), RdNode::Group(body)] = tagged.children() else {
286                return None;
287            };
288            Some(RdSection {
289                title: title.children(),
290                body: body.children(),
291            })
292        })
293    }
294
295    /// Lossy: iterates the `\item{name}{description}` entries of the first
296    /// top-level `\arguments{...}` node, in source order.
297    ///
298    /// Only the FIRST top-level `Tagged` node with `RdTag::Arguments`
299    /// node is considered (first-wins on duplicates, like
300    /// [`RdDocument::title`]); its DIRECT children are then scanned --
301    /// nested `\arguments` are not (there is no such thing in valid Rd,
302    /// but this function makes no attempt to look for one either way).
303    ///
304    /// A direct child yields an [`RdArgument`] only when it matches
305    /// a `Tagged` node with `RdTag::Item`, no option, and the given children where
306    /// `children` has exactly two `RdNode::Group` children (the
307    /// name/description positional-argument groups the two-argument form of
308    /// `\item` lowers to). Everything else is
309    /// silently skipped: the whitespace `TEXT` leaves between `\item`s,
310    /// an `\item[label]` with an option (the zero/one-argument
311    /// `\itemize`/`\enumerate` form, not the two-argument
312    /// `\arguments`/`\describe` form), an `\item` with the wrong number of
313    /// children, and any other non-`\item` markup. This is a total
314    /// function -- it never errors, it just yields fewer items than the
315    /// source `\arguments` block might structurally contain.
316    ///
317    /// If `\arguments` itself isn't found as a top-level `Tagged` node
318    /// (see the module-level documentation on why `Raw` is never
319    /// interpreted), this yields no items.
320    pub fn arguments(&self) -> impl Iterator<Item = RdArgument<'_>> {
321        let children = self.first_tagged_children(RdTag::Arguments).unwrap_or(&[]);
322        children.iter().filter_map(|node| {
323            let tagged = node.as_tagged()?;
324            if tagged.tag() != &RdTag::Item || tagged.option().is_some() {
325                return None;
326            }
327            let [RdNode::Group(name), RdNode::Group(description)] = tagged.children() else {
328                return None;
329            };
330            Some(RdArgument {
331                name: name.children(),
332                description: description.children(),
333            })
334        })
335    }
336
337    /// Strict counterpart to the lossy [`Self::title`] accessor.
338    pub fn inspect_title(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
339        self.inspect_fixed(RdTag::Title)
340    }
341    /// Strict counterpart to the lossy [`Self::description`] accessor.
342    pub fn inspect_description(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
343        self.inspect_fixed(RdTag::Description)
344    }
345    /// Strict counterpart to the lossy [`Self::usage`] accessor.
346    pub fn inspect_usage(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
347        self.inspect_fixed(RdTag::Usage)
348    }
349    /// Strict counterpart to the lossy [`Self::value`] accessor.
350    pub fn inspect_value(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
351        self.inspect_fixed(RdTag::Value)
352    }
353
354    /// Strict counterpart to the lossy [`Self::name`] accessor.
355    pub fn inspect_name(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
356        self.inspect_fixed(RdTag::Name)
357    }
358    /// Strict counterpart to the lossy [`Self::details`] accessor.
359    pub fn inspect_details(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
360        self.inspect_fixed(RdTag::Details)
361    }
362    /// Strict counterpart to the lossy [`Self::note`] accessor.
363    pub fn inspect_note(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
364        self.inspect_fixed(RdTag::Note)
365    }
366    /// Strict counterpart to the lossy [`Self::author`] accessor.
367    pub fn inspect_author(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
368        self.inspect_fixed(RdTag::Author)
369    }
370    /// Strict counterpart to the lossy [`Self::references`] accessor.
371    pub fn inspect_references(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
372        self.inspect_fixed(RdTag::References)
373    }
374    /// Strict counterpart to the lossy [`Self::see_also`] accessor.
375    pub fn inspect_see_also(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
376        self.inspect_fixed(RdTag::SeeAlso)
377    }
378    /// Strict counterpart to the lossy [`Self::examples`] accessor.
379    pub fn inspect_examples(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
380        self.inspect_fixed(RdTag::Examples)
381    }
382    /// Strict counterpart to the lossy [`Self::format`] accessor.
383    pub fn inspect_format(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
384        self.inspect_fixed(RdTag::Format)
385    }
386    /// Strict counterpart to the lossy [`Self::source`] accessor.
387    pub fn inspect_source(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
388        self.inspect_fixed(RdTag::Source)
389    }
390    /// Strict counterpart to the lossy [`Self::encoding`] accessor.
391    pub fn inspect_encoding(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
392        self.inspect_fixed(RdTag::Encoding)
393    }
394    /// Strict counterpart to the lossy [`Self::doc_type`] accessor.
395    pub fn inspect_doc_type(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
396        self.inspect_fixed(RdTag::DocType)
397    }
398    /// Strict counterpart to the lossy [`Self::rd_version`] accessor.
399    pub fn inspect_rd_version(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
400        self.inspect_fixed(RdTag::Rdversion)
401    }
402    /// Strict counterpart to the lossy [`Self::synopsis`] accessor.
403    pub fn inspect_synopsis(&self) -> Result<Option<&[RdNode]>, RdShapeError> {
404        self.inspect_fixed(RdTag::Synopsis)
405    }
406
407    fn inspect_fixed(&self, wanted: RdTag) -> Result<Option<&[RdNode]>, RdShapeError> {
408        let mut found: Option<(usize, &[RdNode])> = None;
409        for (index, node) in self.nodes().iter().enumerate() {
410            let path = top_path(index);
411            if let Some(raw) = node.as_raw() {
412                if raw.tag() == Some(wanted.as_rd_tag()) {
413                    return Err(shape(
414                        path,
415                        Some(wanted.clone()),
416                        RdShapeErrorKind::UnexpectedNode {
417                            expected: RdExpectedNode::Tagged,
418                            actual: RdNodeKind::Raw,
419                        },
420                    ));
421                }
422                continue;
423            }
424            let Some(tagged) = node.as_tagged() else {
425                continue;
426            };
427            if tagged.tag() != &wanted {
428                continue;
429            }
430            if let Some((first_index, _)) = found {
431                let first_path = top_path(first_index);
432                return Err(shape(
433                    path,
434                    Some(wanted.clone()),
435                    RdShapeErrorKind::Duplicate {
436                        construct: RdConstruct::Tag(wanted.clone()),
437                        first_path,
438                    },
439                ));
440            }
441            if tagged.option().is_some() {
442                return Err(shape(
443                    path,
444                    Some(wanted.clone()),
445                    RdShapeErrorKind::UnexpectedOption,
446                ));
447            }
448            found = Some((index, tagged.children()));
449        }
450        Ok(found.map(|(_, children)| children))
451    }
452
453    /// Strict counterpart to the lossy [`Self::aliases`] accessor.
454    pub fn inspect_aliases(&self) -> impl Iterator<Item = Result<RdAlias<'_>, RdShapeError>> + '_ {
455        self.nodes().iter().enumerate().filter_map(|(index, node)| {
456            let path = top_path(index);
457            if node
458                .as_raw()
459                .is_some_and(|raw| raw.tag() == Some(RdTag::Alias.as_rd_tag()))
460            {
461                return Some(Err(shape(
462                    path,
463                    Some(RdTag::Alias),
464                    RdShapeErrorKind::UnexpectedNode {
465                        expected: RdExpectedNode::Tagged,
466                        actual: RdNodeKind::Raw,
467                    },
468                )));
469            }
470            let tagged = node.as_tagged()?;
471            if tagged.tag() != &RdTag::Alias {
472                return None;
473            }
474            if tagged.option().is_some() {
475                return Some(Err(shape(
476                    path,
477                    Some(RdTag::Alias),
478                    RdShapeErrorKind::UnexpectedOption,
479                )));
480            }
481            Some(Ok(RdAlias {
482                nodes: tagged.children(),
483            }))
484        })
485    }
486
487    /// Strict counterpart to the lossy [`Self::keywords`] accessor.
488    pub fn inspect_keywords(
489        &self,
490    ) -> impl Iterator<Item = Result<RdKeyword<'_>, RdShapeError>> + '_ {
491        self.nodes().iter().enumerate().filter_map(|(index, node)| {
492            let path = top_path(index);
493            if node
494                .as_raw()
495                .is_some_and(|raw| raw.tag() == Some(RdTag::Keyword.as_rd_tag()))
496            {
497                return Some(Err(shape(
498                    path,
499                    Some(RdTag::Keyword),
500                    RdShapeErrorKind::UnexpectedNode {
501                        expected: RdExpectedNode::Tagged,
502                        actual: RdNodeKind::Raw,
503                    },
504                )));
505            }
506            let tagged = node.as_tagged()?;
507            if tagged.tag() != &RdTag::Keyword {
508                return None;
509            }
510            if tagged.option().is_some() {
511                return Some(Err(shape(
512                    path,
513                    Some(RdTag::Keyword),
514                    RdShapeErrorKind::UnexpectedOption,
515                )));
516            }
517            Some(Ok(RdKeyword {
518                nodes: tagged.children(),
519            }))
520        })
521    }
522
523    /// Strict counterpart to the lossy [`Self::concepts`] accessor.
524    pub fn inspect_concepts(
525        &self,
526    ) -> impl Iterator<Item = Result<RdConcept<'_>, RdShapeError>> + '_ {
527        self.nodes().iter().enumerate().filter_map(|(index, node)| {
528            let path = top_path(index);
529            if node
530                .as_raw()
531                .is_some_and(|raw| raw.tag() == Some(RdTag::Concept.as_rd_tag()))
532            {
533                return Some(Err(shape(
534                    path,
535                    Some(RdTag::Concept),
536                    RdShapeErrorKind::UnexpectedNode {
537                        expected: RdExpectedNode::Tagged,
538                        actual: RdNodeKind::Raw,
539                    },
540                )));
541            }
542            let tagged = node.as_tagged()?;
543            if tagged.tag() != &RdTag::Concept {
544                return None;
545            }
546            if tagged.option().is_some() {
547                return Some(Err(shape(
548                    path,
549                    Some(RdTag::Concept),
550                    RdShapeErrorKind::UnexpectedOption,
551                )));
552            }
553            Some(Ok(RdConcept {
554                nodes: tagged.children(),
555            }))
556        })
557    }
558
559    /// Lossy depth-first preorder traversal of recognized top-level
560    /// `\section` nodes and recognized nested `\subsection` nodes.
561    ///
562    /// Malformed candidates and their descendants are skipped. An orphan
563    /// top-level `\subsection` is outside this view; source-parser diagnostics
564    /// own that condition. See [`Self::sections`] for the top-level-only view.
565    pub fn section_tree(&self) -> impl Iterator<Item = RdSectionVisit<'_>> {
566        let mut visits = Vec::new();
567        for (index, node) in self.nodes().iter().enumerate() {
568            collect_section_visits(
569                node,
570                top_path(index),
571                RdSectionKind::Section,
572                0,
573                false,
574                &mut visits,
575            );
576        }
577        visits.into_iter().filter_map(Result::ok)
578    }
579
580    /// Strict depth-first preorder traversal of recognized top-level
581    /// `\section` nodes and recognized nested `\subsection` nodes.
582    ///
583    /// Each malformed candidate yields one error and its descendants are not
584    /// traversed. An orphan top-level `\subsection` is outside this view;
585    /// source-parser diagnostics own that condition. See [`Self::sections`]
586    /// for the top-level-only view.
587    pub fn inspect_section_tree(
588        &self,
589    ) -> impl Iterator<Item = Result<RdSectionVisit<'_>, RdShapeError>> {
590        let mut visits = Vec::new();
591        for (index, node) in self.nodes().iter().enumerate() {
592            collect_section_visits(
593                node,
594                top_path(index),
595                RdSectionKind::Section,
596                0,
597                true,
598                &mut visits,
599            );
600        }
601        visits.into_iter()
602    }
603
604    /// Strict counterpart to the lossy [`Self::sections`] accessor.
605    pub fn inspect_sections(
606        &self,
607    ) -> impl Iterator<Item = Result<RdSection<'_>, RdShapeError>> + '_ {
608        self.nodes().iter().enumerate().filter_map(|(index, node)| {
609            let path = top_path(index);
610            if node
611                .as_raw()
612                .is_some_and(|raw| raw.tag() == Some(RdTag::Section.as_rd_tag()))
613            {
614                return Some(Err(shape(
615                    path,
616                    Some(RdTag::Section),
617                    RdShapeErrorKind::UnexpectedNode {
618                        expected: RdExpectedNode::Tagged,
619                        actual: RdNodeKind::Raw,
620                    },
621                )));
622            }
623            let tagged = node.as_tagged()?;
624            if tagged.tag() != &RdTag::Section {
625                return None;
626            }
627            if tagged.option().is_some() {
628                return Some(Err(shape(
629                    path,
630                    Some(RdTag::Section),
631                    RdShapeErrorKind::UnexpectedOption,
632                )));
633            }
634            if tagged.children().len() != 2 {
635                return Some(Err(shape(
636                    path,
637                    Some(RdTag::Section),
638                    RdShapeErrorKind::WrongArity {
639                        expected: RdArity::Exactly(2),
640                        actual: tagged.children().len(),
641                    },
642                )));
643            }
644            let [title, body] = tagged.children() else {
645                unreachable!()
646            };
647            for (child_index, child) in tagged.children().iter().enumerate() {
648                if child.as_group().is_none() {
649                    return Some(Err(shape(
650                        child_path(index, child_index),
651                        Some(RdTag::Section),
652                        RdShapeErrorKind::UnexpectedNode {
653                            expected: RdExpectedNode::Group,
654                            actual: RdNodeKind::of(child),
655                        },
656                    )));
657                }
658            }
659            Some(Ok(RdSection {
660                title: title.as_group().unwrap().children(),
661                body: body.as_group().unwrap().children(),
662            }))
663        })
664    }
665
666    /// Strict counterpart to the lossy [`Self::arguments`] accessor.
667    pub fn inspect_arguments(
668        &self,
669    ) -> Result<impl Iterator<Item = Result<RdArgument<'_>, RdShapeError>> + '_, RdShapeError> {
670        let mut container = None;
671        for (index, node) in self.nodes().iter().enumerate() {
672            let path = top_path(index);
673            if node
674                .as_raw()
675                .is_some_and(|raw| raw.tag() == Some(RdTag::Arguments.as_rd_tag()))
676            {
677                return Err(shape(
678                    path,
679                    Some(RdTag::Arguments),
680                    RdShapeErrorKind::UnexpectedNode {
681                        expected: RdExpectedNode::Tagged,
682                        actual: RdNodeKind::Raw,
683                    },
684                ));
685            }
686            let Some(tagged) = node.as_tagged() else {
687                continue;
688            };
689            if tagged.tag() != &RdTag::Arguments {
690                continue;
691            }
692            if container.is_some() {
693                let first_index = container.map(|(index, _)| index).unwrap_or(index);
694                return Err(shape(
695                    path,
696                    Some(RdTag::Arguments),
697                    RdShapeErrorKind::Duplicate {
698                        construct: RdConstruct::Tag(RdTag::Arguments),
699                        first_path: top_path(first_index),
700                    },
701                ));
702            }
703            if tagged.option().is_some() {
704                return Err(shape(
705                    path,
706                    Some(RdTag::Arguments),
707                    RdShapeErrorKind::UnexpectedOption,
708                ));
709            }
710            container = Some((index, tagged.children()));
711        }
712        let parent_index = container.map_or(0, |(index, _)| index);
713        let children = container.map_or(&[][..], |(_, children)| children);
714        Ok(children
715            .iter()
716            .enumerate()
717            .filter_map(move |(index, node)| {
718                if is_inter_item_trivia(node) {
719                    return None;
720                }
721                let path = child_path(parent_index, index);
722                let Some(tagged) = node.as_tagged() else {
723                    return Some(Err(shape(
724                        path,
725                        node_tag(node),
726                        RdShapeErrorKind::UnexpectedContent {
727                            actual: RdNodeKind::of(node),
728                        },
729                    )));
730                };
731                if tagged.tag() != &RdTag::Item {
732                    return Some(Err(shape(
733                        path,
734                        Some(tagged.tag().clone()),
735                        RdShapeErrorKind::UnexpectedContent {
736                            actual: RdNodeKind::Tagged,
737                        },
738                    )));
739                }
740                let (name, description) = match inspect_two_group_item(tagged, &path) {
741                    Ok(groups) => groups,
742                    Err(error) => return Some(Err(error)),
743                };
744                Some(Ok(RdArgument { name, description }))
745            }))
746    }
747}
748
749fn collect_section_visits<'a>(
750    node: &'a RdNode,
751    path: RdPath,
752    kind: RdSectionKind,
753    nesting: usize,
754    strict: bool,
755    output: &mut Vec<Result<RdSectionVisit<'a>, RdShapeError>>,
756) {
757    let wanted = match kind {
758        RdSectionKind::Section => RdTag::Section,
759        RdSectionKind::Subsection => RdTag::Subsection,
760    };
761    let Some(tagged) = node.as_tagged() else {
762        if strict
763            && node
764                .as_raw()
765                .is_some_and(|raw| raw.tag() == Some(wanted.as_rd_tag()))
766        {
767            output.push(Err(shape(
768                path,
769                Some(wanted),
770                RdShapeErrorKind::UnexpectedNode {
771                    expected: RdExpectedNode::Tagged,
772                    actual: RdNodeKind::Raw,
773                },
774            )));
775        }
776        return;
777    };
778    if tagged.tag() != &wanted {
779        return;
780    }
781    if tagged.option().is_some() {
782        if strict {
783            output.push(Err(shape(
784                path,
785                Some(wanted),
786                RdShapeErrorKind::UnexpectedOption,
787            )));
788        }
789        return;
790    }
791    if tagged.children().len() != 2 {
792        if strict {
793            output.push(Err(shape(
794                path,
795                Some(wanted),
796                RdShapeErrorKind::WrongArity {
797                    expected: RdArity::Exactly(2),
798                    actual: tagged.children().len(),
799                },
800            )));
801        }
802        return;
803    }
804    let [title, body] = tagged.children() else {
805        unreachable!()
806    };
807    if let Some((child_index, child)) = tagged
808        .children()
809        .iter()
810        .enumerate()
811        .find(|(_, child)| child.as_group().is_none())
812    {
813        if strict {
814            output.push(Err(shape(
815                path.with_child(child_index),
816                Some(wanted),
817                RdShapeErrorKind::UnexpectedNode {
818                    expected: RdExpectedNode::Group,
819                    actual: RdNodeKind::of(child),
820                },
821            )));
822        }
823        return;
824    }
825    let title = title.as_group().unwrap().children();
826    let body = body.as_group().unwrap().children();
827    output.push(Ok(RdSectionVisit {
828        path: path.clone(),
829        kind,
830        nesting,
831        title,
832        body,
833    }));
834
835    let body_path = path.with_child(1);
836    for (index, child) in body.iter().enumerate() {
837        collect_section_visits(
838            child,
839            body_path.with_child(index),
840            RdSectionKind::Subsection,
841            nesting + 1,
842            strict,
843            output,
844        );
845    }
846}