asciidoc_parser/document/
header.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    content::{Content, SubstitutionGroup},
6    document::{Attribute, Author, AuthorLine, RevisionLine},
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    author_line: Option<AuthorLine<'src>>,
20    revision_line: Option<RevisionLine<'src>>,
21    comments: Vec<Span<'src>>,
22    source: Span<'src>,
23}
24
25impl<'src> Header<'src> {
26    pub(crate) fn parse(
27        mut source: Span<'src>,
28        parser: &mut Parser,
29    ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
30        let original_source = source.discard_empty_lines();
31
32        let mut title_source: Option<Span<'src>> = None;
33        let mut title: Option<String> = None;
34        let mut attributes: Vec<Attribute> = vec![];
35        let mut author_line: Option<AuthorLine<'src>> = None;
36        let mut revision_line: Option<RevisionLine<'src>> = None;
37        let mut comments: Vec<Span<'src>> = vec![];
38        let mut warnings: Vec<Warning<'src>> = vec![];
39
40        // Aside from the title line, items can appear in almost any order.
41        while !source.is_empty() {
42            let line_mi = source.take_normalized_line();
43            let line = line_mi.item;
44
45            // A blank line after the title ends the header.
46            if line.is_empty() {
47                if title.is_some() {
48                    break;
49                }
50                source = line_mi.after;
51            } else if line.starts_with("//") && !line.starts_with("///") {
52                comments.push(line);
53                source = line_mi.after;
54            } else if line.starts_with(':')
55                && let Some(attr) = Attribute::parse(source, parser)
56            {
57                // Special handling for :author: attribute to populate individual author
58                // attributes.
59                if attr.item.name().data().eq_ignore_ascii_case("author")
60                    && let Some(raw_value) = attr.item.raw_value()
61                    && let Some(author) = Author::parse(raw_value.data(), parser)
62                {
63                    // Set individual author attributes.
64                    parser.set_attribute_by_value_from_header("firstname", author.firstname());
65                    if let Some(middlename) = author.middlename() {
66                        parser.set_attribute_by_value_from_header("middlename", middlename);
67                    }
68                    if let Some(lastname) = author.lastname() {
69                        parser.set_attribute_by_value_from_header("lastname", lastname);
70                    }
71                    parser.set_attribute_by_value_from_header("authorinitials", author.initials());
72                    if let Some(email) = author.email() {
73                        parser.set_attribute_by_value_from_header("email", email);
74                    }
75                }
76
77                parser.set_attribute_from_header(&attr.item, &mut warnings);
78                attributes.push(attr.item);
79                source = attr.after;
80            } else if title.is_none() && line.starts_with("= ") {
81                let title_span = line.discard(2).discard_whitespace();
82                let title_str = apply_header_subs(title_span.data(), parser);
83
84                parser.set_attribute_by_value_from_header("doctitle", &title_str);
85
86                title = Some(title_str);
87                title_source = Some(title_span);
88                source = line_mi.after;
89            } else if title.is_some() && author_line.is_none() {
90                author_line = Some(AuthorLine::parse(line, parser));
91                source = line_mi.after;
92            } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
93                revision_line = Some(RevisionLine::parse(line, parser));
94                source = line_mi.after;
95            } else {
96                if title.is_some() {
97                    warnings.push(Warning {
98                        source: line,
99                        warning: WarningType::DocumentHeaderNotTerminated,
100                    });
101                }
102                break;
103            }
104        }
105
106        let after = source.discard_empty_lines();
107        let source = original_source.trim_remainder(source);
108
109        MatchAndWarnings {
110            item: MatchedItem {
111                item: Self {
112                    title_source,
113                    title,
114                    attributes,
115                    author_line,
116                    revision_line,
117                    comments,
118                    source: source.trim_trailing_whitespace(),
119                },
120                after,
121            },
122            warnings,
123        }
124    }
125
126    /// Return a [`Span`] describing the raw document title, if there was one.
127    pub fn title_source(&'src self) -> Option<Span<'src>> {
128        self.title_source
129    }
130
131    /// Return the document's title, if there was one, having applied header
132    /// substitutions.
133    pub fn title(&self) -> Option<&str> {
134        self.title.as_deref()
135    }
136
137    /// Return an iterator over the attributes in this header.
138    pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
139        self.attributes.iter()
140    }
141
142    /// Returns the author line, if found.
143    pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
144        self.author_line.as_ref()
145    }
146
147    /// Returns the revision line, if found.
148    pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
149        self.revision_line.as_ref()
150    }
151
152    /// Return an iterator over the comments in this header.
153    pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
154        self.comments.iter()
155    }
156}
157
158impl<'src> HasSpan<'src> for Header<'src> {
159    fn span(&self) -> Span<'src> {
160        self.source
161    }
162}
163
164fn apply_header_subs(source: &str, parser: &Parser) -> String {
165    let span = Span::new(source);
166
167    let mut content = Content::from(span);
168    SubstitutionGroup::Header.apply(&mut content, parser, None);
169
170    content.rendered().to_string()
171}