asciidoc_parser/document/
header.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    content::{Content, SubstitutionGroup},
6    document::Attribute,
7    span::MatchedItem,
8    warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11/// An AsciiDoc document may begin with a document header. The document header
12/// encapsulates the document title, author and revision information,
13/// document-wide attributes, and other document metadata.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Header<'src> {
16    title_source: Option<Span<'src>>,
17    title: Option<String>,
18    attributes: Vec<Attribute<'src>>,
19    source: Span<'src>,
20}
21
22impl<'src> Header<'src> {
23    pub(crate) fn parse(
24        source: Span<'src>,
25        parser: &mut Parser,
26    ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
27        let original_src = source;
28
29        let mut attributes: Vec<Attribute> = vec![];
30        let mut warnings: Vec<Warning<'src>> = vec![];
31
32        let source = source.discard_empty_lines();
33
34        let (title_source, mut after) = if let Some(mi) = parse_title(source) {
35            (Some(mi.item), mi.after)
36        } else {
37            (None, source)
38        };
39
40        let title = title_source.map(|ref span| {
41            let mut content = Content::from(*span);
42            SubstitutionGroup::Header.apply(&mut content, parser, None);
43            content.rendered.into_string()
44        });
45
46        while let Some(attr) = Attribute::parse(after, parser) {
47            parser.set_attribute_from_header(&attr.item, &mut warnings);
48            attributes.push(attr.item);
49            after = attr.after;
50        }
51
52        let source = source.trim_remainder(after);
53
54        // Nothing resembling a header so far? Don't look for empty line.
55        if title_source.is_none() && attributes.is_empty() {
56            return MatchAndWarnings {
57                item: MatchedItem {
58                    item: Self {
59                        title_source: None,
60                        title: None,
61                        attributes,
62                        source: original_src.into_parse_result(0).item,
63                    },
64                    after,
65                },
66                warnings,
67            };
68        }
69
70        // Header is valid so far. Warn if not followed by empty line or EOF.
71        after = match after.take_empty_line() {
72            Some(mi) => mi.after.discard_empty_lines(),
73            None => {
74                warnings.push(Warning {
75                    source: after.take_line().item,
76                    warning: WarningType::DocumentHeaderNotTerminated,
77                });
78                after
79            }
80        };
81
82        MatchAndWarnings {
83            item: MatchedItem {
84                item: Self {
85                    title_source,
86                    title,
87                    attributes,
88                    source: source.trim_trailing_whitespace(),
89                },
90                after,
91            },
92            warnings,
93        }
94    }
95
96    /// Return a [`Span`] describing the raw document title, if there was one.
97    pub fn title_source(&'src self) -> Option<Span<'src>> {
98        self.title_source
99    }
100
101    /// Return the document's title, if there was one, having applied header
102    /// substitutions.
103    pub fn title(&self) -> Option<&str> {
104        self.title.as_deref()
105    }
106
107    /// Return an iterator over the attributes in this header.
108    pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
109        self.attributes.iter()
110    }
111}
112
113impl<'src> HasSpan<'src> for Header<'src> {
114    fn span(&self) -> Span<'src> {
115        self.source
116    }
117}
118
119fn parse_title(source: Span<'_>) -> Option<MatchedItem<'_, Span<'_>>> {
120    let line = source.take_non_empty_line()?;
121    let equal = line.item.take_prefix("=")?;
122    let ws = equal.after.take_required_whitespace()?;
123
124    Some(MatchedItem {
125        item: ws.after,
126        after: line.after,
127    })
128}