Skip to main content

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    internal::debug::DebugSliceReference,
8    span::MatchedItem,
9    warnings::{MatchAndWarnings, Warning, WarningType},
10};
11
12/// An AsciiDoc document may begin with a document header. The document header
13/// encapsulates the document title, author and revision information,
14/// document-wide attributes, and other document metadata.
15#[derive(Clone, Eq, PartialEq)]
16pub struct Header<'src> {
17    title_source: Option<Span<'src>>,
18    title: Option<String>,
19    attributes: Vec<Attribute<'src>>,
20    author_line: Option<AuthorLine<'src>>,
21    revision_line: Option<RevisionLine<'src>>,
22    comments: Vec<Span<'src>>,
23    source: Span<'src>,
24}
25
26impl<'src> Header<'src> {
27    pub(crate) fn parse(
28        mut source: Span<'src>,
29        parser: &mut Parser,
30    ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
31        let original_source = source.discard_empty_lines();
32
33        let mut title_source: Option<Span<'src>> = None;
34        let mut title: Option<String> = None;
35        let mut attributes: Vec<Attribute> = vec![];
36        let mut author_line: Option<AuthorLine<'src>> = None;
37        let mut revision_line: Option<RevisionLine<'src>> = None;
38        let mut comments: Vec<Span<'src>> = vec![];
39        let mut warnings: Vec<Warning<'src>> = vec![];
40
41        // Aside from the title line, items can appear in almost any order.
42        while !source.is_empty() {
43            let line_mi = source.take_normalized_line();
44            let line = line_mi.item;
45
46            // A blank line after the title ends the header.
47            if line.is_empty() {
48                if title.is_some() {
49                    break;
50                }
51                source = line_mi.after;
52            } else if line.starts_with("//") && !line.starts_with("///") {
53                comments.push(line);
54                source = line_mi.after;
55            } else if line.starts_with(':')
56                && let Some(attr) = Attribute::parse(source, parser)
57            {
58                // Special handling for :author: attribute to populate individual author
59                // attributes.
60                if attr.item.name().data().eq_ignore_ascii_case("author")
61                    && let Some(raw_value) = attr.item.raw_value()
62                    && let Some(author) = Author::parse(raw_value.data(), parser)
63                {
64                    // Set individual author attributes.
65                    parser.set_attribute_by_value_from_header("firstname", author.firstname());
66                    if let Some(middlename) = author.middlename() {
67                        parser.set_attribute_by_value_from_header("middlename", middlename);
68                    }
69                    if let Some(lastname) = author.lastname() {
70                        parser.set_attribute_by_value_from_header("lastname", lastname);
71                    }
72                    parser.set_attribute_by_value_from_header("authorinitials", author.initials());
73                    if let Some(email) = author.email() {
74                        parser.set_attribute_by_value_from_header("email", email);
75                    }
76                }
77
78                parser.set_attribute_from_header(&attr.item, &mut warnings);
79                attributes.push(attr.item);
80                source = attr.after;
81            } else if title.is_none() && line.starts_with("= ") {
82                let title_span = line.discard(2).discard_whitespace();
83                let title_str = apply_header_subs(title_span.data(), parser);
84
85                parser.set_attribute_by_value_from_header("doctitle", &title_str);
86
87                title = Some(title_str);
88                title_source = Some(title_span);
89                source = line_mi.after;
90            } else if title.is_some() && author_line.is_none() {
91                author_line = Some(AuthorLine::parse(line, parser));
92                source = line_mi.after;
93            } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
94                revision_line = Some(RevisionLine::parse(line, parser));
95                source = line_mi.after;
96            } else {
97                if title.is_some() {
98                    warnings.push(Warning {
99                        source: line,
100                        warning: WarningType::DocumentHeaderNotTerminated,
101                    });
102                }
103                break;
104            }
105        }
106
107        let after = source.discard_empty_lines();
108        let source = original_source.trim_remainder(source);
109
110        MatchAndWarnings {
111            item: MatchedItem {
112                item: Self {
113                    title_source,
114                    title,
115                    attributes,
116                    author_line,
117                    revision_line,
118                    comments,
119                    source: source.trim_trailing_whitespace(),
120                },
121                after,
122            },
123            warnings,
124        }
125    }
126
127    /// Return a [`Span`] describing the raw document title, if there was one.
128    pub fn title_source(&'src self) -> Option<Span<'src>> {
129        self.title_source
130    }
131
132    /// Return the document's title, if there was one, having applied header
133    /// substitutions.
134    pub fn title(&self) -> Option<&str> {
135        self.title.as_deref()
136    }
137
138    /// Return an iterator over the attributes in this header.
139    pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
140        self.attributes.iter()
141    }
142
143    /// Returns the author line, if found.
144    pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
145        self.author_line.as_ref()
146    }
147
148    /// Returns the revision line, if found.
149    pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
150        self.revision_line.as_ref()
151    }
152
153    /// Return an iterator over the comments in this header.
154    pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
155        self.comments.iter()
156    }
157}
158
159impl<'src> HasSpan<'src> for Header<'src> {
160    fn span(&self) -> Span<'src> {
161        self.source
162    }
163}
164
165fn apply_header_subs(source: &str, parser: &Parser) -> String {
166    let span = Span::new(source);
167
168    let mut content = Content::from(span);
169    SubstitutionGroup::Header.apply(&mut content, parser, None);
170
171    content.rendered().to_string()
172}
173
174impl std::fmt::Debug for Header<'_> {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.debug_struct("Header")
177            .field("title_source", &self.title_source)
178            .field("title", &self.title)
179            .field("attributes", &DebugSliceReference(&self.attributes))
180            .field("author_line", &self.author_line)
181            .field("revision_line", &self.revision_line)
182            .field("comments", &DebugSliceReference(&self.comments))
183            .field("source", &self.source)
184            .finish()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    #![allow(clippy::unwrap_used)]
191
192    use crate::tests::prelude::*;
193
194    #[test]
195    fn impl_clone() {
196        // Silly test to mark the #[derive(...)] line as covered.
197        let mut parser = Parser::default();
198
199        let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
200            .unwrap_if_no_warnings();
201        let h2 = h1.clone();
202
203        assert_eq!(h1, h2);
204    }
205
206    #[test]
207    fn only_title() {
208        let mut parser = Parser::default();
209        let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
210            .unwrap_if_no_warnings();
211
212        assert_eq!(
213            mi.item,
214            Header {
215                title_source: Some(Span {
216                    data: "Just the Title",
217                    line: 1,
218                    col: 3,
219                    offset: 2,
220                }),
221                title: Some("Just the Title"),
222                attributes: &[],
223                author_line: None,
224                revision_line: None,
225                comments: &[],
226                source: Span {
227                    data: "= Just the Title",
228                    line: 1,
229                    col: 1,
230                    offset: 0,
231                }
232            }
233        );
234
235        assert_eq!(
236            mi.after,
237            Span {
238                data: "",
239                line: 1,
240                col: 17,
241                offset: 16
242            }
243        );
244    }
245
246    #[test]
247    fn trims_leading_spaces_in_title() {
248        // This is totally a judgement call on my part. As far as I can tell,
249        // the language doesn't describe behavior here.
250        let mut parser = Parser::default();
251        let mi =
252            crate::document::Header::parse(crate::Span::new("=    Just the Title"), &mut parser)
253                .unwrap_if_no_warnings();
254
255        assert_eq!(
256            mi.item,
257            Header {
258                title_source: Some(Span {
259                    data: "Just the Title",
260                    line: 1,
261                    col: 6,
262                    offset: 5,
263                }),
264                title: Some("Just the Title"),
265                attributes: &[],
266                author_line: None,
267                revision_line: None,
268                comments: &[],
269                source: Span {
270                    data: "=    Just the Title",
271                    line: 1,
272                    col: 1,
273                    offset: 0,
274                }
275            }
276        );
277
278        assert_eq!(
279            mi.after,
280            Span {
281                data: "",
282                line: 1,
283                col: 20,
284                offset: 19
285            }
286        );
287    }
288
289    #[test]
290    fn trims_trailing_spaces_in_title() {
291        let mut parser = Parser::default();
292        let mi =
293            crate::document::Header::parse(crate::Span::new("= Just the Title   "), &mut parser)
294                .unwrap_if_no_warnings();
295
296        assert_eq!(
297            mi.item,
298            Header {
299                title_source: Some(Span {
300                    data: "Just the Title",
301                    line: 1,
302                    col: 3,
303                    offset: 2,
304                }),
305                title: Some("Just the Title"),
306                attributes: &[],
307                author_line: None,
308                revision_line: None,
309                comments: &[],
310                source: Span {
311                    data: "= Just the Title",
312                    line: 1,
313                    col: 1,
314                    offset: 0,
315                }
316            }
317        );
318
319        assert_eq!(
320            mi.after,
321            Span {
322                data: "",
323                line: 1,
324                col: 20,
325                offset: 19
326            }
327        );
328    }
329
330    #[test]
331    fn title_and_attribute() {
332        let mut parser = Parser::default();
333
334        let mi = crate::document::Header::parse(
335            crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
336            &mut parser,
337        )
338        .unwrap_if_no_warnings();
339
340        assert_eq!(
341            mi.item,
342            Header {
343                title_source: Some(Span {
344                    data: "Just the Title",
345                    line: 1,
346                    col: 3,
347                    offset: 2,
348                }),
349                title: Some("Just the Title"),
350                attributes: &[Attribute {
351                    name: Span {
352                        data: "foo",
353                        line: 2,
354                        col: 2,
355                        offset: 18,
356                    },
357                    value_source: Some(Span {
358                        data: "bar",
359                        line: 2,
360                        col: 7,
361                        offset: 23,
362                    }),
363                    value: InterpretedValue::Value("bar"),
364                    source: Span {
365                        data: ":foo: bar",
366                        line: 2,
367                        col: 1,
368                        offset: 17,
369                    }
370                }],
371                author_line: None,
372                revision_line: None,
373                comments: &[],
374                source: Span {
375                    data: "= Just the Title\n:foo: bar",
376                    line: 1,
377                    col: 1,
378                    offset: 0,
379                }
380            }
381        );
382
383        assert_eq!(
384            mi.after,
385            Span {
386                data: "blah",
387                line: 4,
388                col: 1,
389                offset: 28
390            }
391        );
392    }
393
394    #[test]
395    fn title_applies_header_substitutions() {
396        let mut parser = Parser::default();
397
398        let mi = crate::document::Header::parse(
399            crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
400            &mut parser,
401        )
402        .unwrap_if_no_warnings();
403
404        assert_eq!(
405            mi.item,
406            Header {
407                title_source: Some(Span {
408                    data: "The Title & Some{sp}Nonsense",
409                    line: 1,
410                    col: 3,
411                    offset: 2,
412                }),
413                title: Some("The Title &amp; Some Nonsense"),
414                attributes: &[Attribute {
415                    name: Span {
416                        data: "foo",
417                        line: 2,
418                        col: 2,
419                        offset: 32,
420                    },
421                    value_source: Some(Span {
422                        data: "bar",
423                        line: 2,
424                        col: 7,
425                        offset: 37,
426                    }),
427                    value: InterpretedValue::Value("bar"),
428                    source: Span {
429                        data: ":foo: bar",
430                        line: 2,
431                        col: 1,
432                        offset: 31,
433                    }
434                }],
435                author_line: None,
436                revision_line: None,
437                comments: &[],
438                source: Span {
439                    data: "= The Title & Some{sp}Nonsense\n:foo: bar",
440                    line: 1,
441                    col: 1,
442                    offset: 0,
443                }
444            }
445        );
446
447        assert_eq!(
448            mi.after,
449            Span {
450                data: "blah",
451                line: 4,
452                col: 1,
453                offset: 42
454            }
455        );
456    }
457
458    #[test]
459    fn attribute_without_title() {
460        let mut parser = Parser::default();
461        let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
462            .unwrap_if_no_warnings();
463
464        assert_eq!(
465            mi.item,
466            Header {
467                title_source: None,
468                title: None,
469                attributes: &[Attribute {
470                    name: Span {
471                        data: "foo",
472                        line: 1,
473                        col: 2,
474                        offset: 1,
475                    },
476                    value_source: Some(Span {
477                        data: "bar",
478                        line: 1,
479                        col: 7,
480                        offset: 6,
481                    }),
482                    value: InterpretedValue::Value("bar"),
483                    source: Span {
484                        data: ":foo: bar",
485                        line: 1,
486                        col: 1,
487                        offset: 0,
488                    }
489                }],
490                author_line: None,
491                revision_line: None,
492                comments: &[],
493                source: Span {
494                    data: ":foo: bar",
495                    line: 1,
496                    col: 1,
497                    offset: 0,
498                }
499            }
500        );
501
502        assert_eq!(
503            mi.after,
504            Span {
505                data: "blah",
506                line: 3,
507                col: 1,
508                offset: 11
509            }
510        );
511    }
512
513    #[test]
514    fn sets_doctitle_attribute() {
515        let mut parser = Parser::default();
516        let _doc = parser.parse("= Document Title Goes Here");
517
518        assert_eq!(
519            parser.attribute_value("doctitle"),
520            InterpretedValue::Value("Document Title Goes Here")
521        );
522    }
523
524    #[test]
525    fn sets_author_attributes_from_author_attribute() {
526        let mut parser = Parser::default();
527        let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
528
529        // Verify that individual author attributes are set.
530        assert_eq!(
531            parser.attribute_value("firstname"),
532            InterpretedValue::Value("John")
533        );
534        assert_eq!(
535            parser.attribute_value("middlename"),
536            InterpretedValue::Value("Q.")
537        );
538        assert_eq!(
539            parser.attribute_value("lastname"),
540            InterpretedValue::Value("Smith")
541        );
542        assert_eq!(
543            parser.attribute_value("authorinitials"),
544            InterpretedValue::Value("JQS")
545        );
546        assert_eq!(
547            parser.attribute_value("email"),
548            InterpretedValue::Value("john@example.com")
549        );
550
551        // Also verify the original author attribute is still set (with HTML encoding).
552        assert_eq!(
553            parser.attribute_value("author"),
554            InterpretedValue::Value("John Q. Smith &lt;john@example.com&gt;")
555        );
556    }
557
558    #[test]
559    fn sets_author_attributes_from_author_attribute_two_names() {
560        let mut parser = Parser::default();
561        let _doc = parser.parse(":author: Jane Doe");
562
563        // Verify that individual author attributes are set.
564        assert_eq!(
565            parser.attribute_value("firstname"),
566            InterpretedValue::Value("Jane")
567        );
568        assert_eq!(
569            parser.attribute_value("middlename"),
570            InterpretedValue::Unset
571        );
572        assert_eq!(
573            parser.attribute_value("lastname"),
574            InterpretedValue::Value("Doe")
575        );
576        assert_eq!(
577            parser.attribute_value("authorinitials"),
578            InterpretedValue::Value("JD")
579        );
580        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
581    }
582
583    #[test]
584    fn sets_author_attributes_from_author_attribute_single_name() {
585        let mut parser = Parser::default();
586        let _doc = parser.parse(":author: Cher");
587
588        // Verify that individual author attributes are set.
589        assert_eq!(
590            parser.attribute_value("firstname"),
591            InterpretedValue::Value("Cher")
592        );
593        assert_eq!(
594            parser.attribute_value("middlename"),
595            InterpretedValue::Unset
596        );
597        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
598        assert_eq!(
599            parser.attribute_value("authorinitials"),
600            InterpretedValue::Value("C")
601        );
602        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
603    }
604
605    #[test]
606    fn sets_author_attributes_from_empty_string() {
607        let mut parser = Parser::default();
608        let _doc = parser.parse(":author:");
609
610        // Verify that individual author attributes are set.
611        assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
612        assert_eq!(
613            parser.attribute_value("middlename"),
614            InterpretedValue::Unset
615        );
616        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
617        assert_eq!(
618            parser.attribute_value("authorinitials"),
619            InterpretedValue::Unset
620        );
621        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
622
623        assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
624    }
625
626    #[test]
627    fn impl_debug() {
628        let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
629        let header = doc.header();
630
631        assert_eq!(
632            format!("{header:#?}"),
633            r#"Header {
634    title_source: Some(
635        Span {
636            data: "Example Title",
637            line: 1,
638            col: 3,
639            offset: 2,
640        },
641    ),
642    title: Some(
643        "Example Title",
644    ),
645    attributes: &[],
646    author_line: None,
647    revision_line: None,
648    comments: &[],
649    source: Span {
650        data: "= Example Title",
651        line: 1,
652        col: 1,
653        offset: 0,
654    },
655}"#
656        );
657    }
658}