asciidoc_parser/blocks/
section.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    attributes::Attrlist,
6    blocks::{
7        Block, ContentModel, IsBlock, metadata::BlockMetadata, parse_utils::parse_blocks_until,
8    },
9    content::{Content, SubstitutionGroup},
10    span::MatchedItem,
11    strings::CowStr,
12    warnings::MatchAndWarnings,
13};
14
15/// Sections partition the document into a content hierarchy. A section is an
16/// implicit enclosure. Each section begins with a title and ends at the next
17/// sibling section, ancestor section, or end of document. Nested section levels
18/// must be sequential.
19///
20/// **WARNING:** This is a very preliminary implementation. There are many **TO
21/// DO** items in this code.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct SectionBlock<'src> {
24    level: usize,
25    section_title: Content<'src>,
26    blocks: Vec<Block<'src>>,
27    source: Span<'src>,
28    title_source: Option<Span<'src>>,
29    title: Option<String>,
30    anchor: Option<Span<'src>>,
31    attrlist: Option<Attrlist<'src>>,
32}
33
34impl<'src> SectionBlock<'src> {
35    pub(crate) fn parse(
36        metadata: &BlockMetadata<'src>,
37        parser: &mut Parser,
38    ) -> Option<MatchAndWarnings<'src, MatchedItem<'src, Self>>> {
39        let source = metadata.block_start.discard_empty_lines();
40        let level = parse_title_line(source)?;
41
42        let maw_blocks = parse_blocks_until(
43            level.after,
44            |i| peer_or_ancestor_section(*i, level.item.0),
45            parser,
46        );
47
48        let blocks = maw_blocks.item;
49        let source = metadata.source.trim_remainder(blocks.after);
50
51        let mut section_title = Content::from(level.item.1);
52        SubstitutionGroup::Title.apply(&mut section_title, parser, metadata.attrlist.as_ref());
53
54        Some(MatchAndWarnings {
55            item: MatchedItem {
56                item: Self {
57                    level: level.item.0,
58                    section_title,
59                    blocks: blocks.item,
60                    source: source.trim_trailing_whitespace(),
61                    title_source: metadata.title_source,
62                    title: metadata.title.clone(),
63                    anchor: metadata.anchor,
64                    attrlist: metadata.attrlist.clone(),
65                },
66                after: blocks.after,
67            },
68            warnings: maw_blocks.warnings,
69        })
70    }
71
72    /// Return the section's level.
73    ///
74    /// The section title must be prefixed with a section marker, which
75    /// indicates the section level. The number of equal signs in the marker
76    /// represents the section level using a 0-based index (e.g., two equal
77    /// signs represents level 1). A section marker can range from two to six
78    /// equal signs and must be followed by a space.
79    ///
80    /// This function will return an integer between 1 and 5.
81    pub fn level(&self) -> usize {
82        self.level
83    }
84
85    /// Return a [`Span`] containing the section title source.
86    pub fn section_title_source(&self) -> Span<'src> {
87        self.section_title.original()
88    }
89
90    /// Return the processed section title after substitutions have been
91    /// applied.
92    pub fn section_title(&'src self) -> &'src str {
93        self.section_title.rendered()
94    }
95}
96
97impl<'src> IsBlock<'src> for SectionBlock<'src> {
98    fn content_model(&self) -> ContentModel {
99        ContentModel::Compound
100    }
101
102    fn raw_context(&self) -> CowStr<'src> {
103        "section".into()
104    }
105
106    fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
107        self.blocks.iter()
108    }
109
110    fn title_source(&'src self) -> Option<Span<'src>> {
111        self.title_source
112    }
113
114    fn title(&self) -> Option<&str> {
115        self.title.as_deref()
116    }
117
118    fn anchor(&'src self) -> Option<Span<'src>> {
119        self.anchor
120    }
121
122    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
123        self.attrlist.as_ref()
124    }
125}
126
127impl<'src> HasSpan<'src> for SectionBlock<'src> {
128    fn span(&self) -> Span<'src> {
129        self.source
130    }
131}
132
133fn parse_title_line(source: Span<'_>) -> Option<MatchedItem<'_, (usize, Span<'_>)>> {
134    let mi = source.take_non_empty_line()?;
135    let mut line = mi.item;
136
137    // TO DO: Also support Markdown-style `#` markers.
138    // TO DO: Enforce maximum of 6 `=` or `#` markers.
139    // TO DO: Disallow empty title.
140
141    let mut count = 0;
142
143    while let Some(mi) = line.take_prefix("=") {
144        count += 1;
145        line = mi.after;
146    }
147
148    let title = line.take_required_whitespace()?;
149
150    Some(MatchedItem {
151        item: (count - 1, title.after),
152        after: mi.after,
153    })
154}
155
156fn peer_or_ancestor_section(source: Span<'_>, level: usize) -> bool {
157    if let Some(mi) = parse_title_line(source) {
158        mi.item.0 <= level
159    } else {
160        false
161    }
162}