asciidoc_parser/document/
header.rs

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