asciidoc_parser/blocks/
section.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use std::slice::Iter;

use crate::{
    blocks::{parse_utils::parse_blocks_until, Block, ContentModel, IsBlock},
    span::MatchedItem,
    strings::CowStr,
    warnings::MatchAndWarnings,
    HasSpan, Span,
};

/// Sections partition the document into a content hierarchy. A section is an
/// implicit enclosure. Each section begins with a title and ends at the next
/// sibling section, ancestor section, or end of document. Nested section levels
/// must be sequential.
///
/// WARNING: This is a very preliminary implementation. There are many TO DO
/// items in this code.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SectionBlock<'src> {
    level: usize,
    title: Span<'src>,
    blocks: Vec<Block<'src>>,
    source: Span<'src>,
}

impl<'src> SectionBlock<'src> {
    pub(crate) fn parse(
        source: Span<'src>,
    ) -> Option<MatchAndWarnings<'src, MatchedItem<'src, Self>>> {
        let source = source.discard_empty_lines();
        let level = parse_title_line(source)?;

        let maw_blocks =
            parse_blocks_until(level.after, |i| peer_or_ancestor_section(*i, level.item.0));

        let blocks = maw_blocks.item;
        let source = source.trim_remainder(blocks.after);

        Some(MatchAndWarnings {
            item: MatchedItem {
                item: Self {
                    level: level.item.0,
                    title: level.item.1,
                    blocks: blocks.item,
                    source,
                },
                after: blocks.after,
            },
            warnings: maw_blocks.warnings,
        })
    }

    /// Return the section's level.
    ///
    /// The section title must be prefixed with a section marker, which
    /// indicates the section level. The number of equal signs in the marker
    /// represents the section level using a 0-based index (e.g., two equal
    /// signs represents level 1). A section marker can range from two to six
    /// equal signs and must be followed by a space.
    ///
    /// This function will return an integer between 1 and 5.
    pub fn level(&self) -> usize {
        self.level
    }

    /// Return a [`Span`] describing the section title.
    pub fn title(&'src self) -> &'src Span<'src> {
        &self.title
    }
}

impl<'src> IsBlock<'src> for SectionBlock<'src> {
    fn content_model(&self) -> ContentModel {
        ContentModel::Compound
    }

    fn context(&self) -> CowStr<'src> {
        "section".into()
    }

    fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
        self.blocks.iter()
    }
}

impl<'src> HasSpan<'src> for SectionBlock<'src> {
    fn span(&'src self) -> &'src Span<'src> {
        &self.source
    }
}

fn parse_title_line(source: Span<'_>) -> Option<MatchedItem<(usize, Span)>> {
    let mi = source.take_non_empty_line()?;
    let mut line = mi.item;

    // TO DO: Also support Markdown-style `#` markers.
    // TO DO: Enforce maximum of 6 `=` or `#` markers.
    // TO DO: Disallow empty title.

    let mut count = 0;

    while let Some(mi) = line.take_prefix("=") {
        count += 1;
        line = mi.after;
    }

    let title = line.take_required_whitespace()?;

    Some(MatchedItem {
        item: (count - 1, title.after),
        after: mi.after,
    })
}

fn peer_or_ancestor_section(source: Span<'_>, level: usize) -> bool {
    if let Some(mi) = parse_title_line(source) {
        mi.item.0 <= level
    } else {
        false
    }
}