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