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 pretty_assertions_sorted::assert_eq;
193
194    use crate::{Parser, tests::prelude::*};
195
196    #[test]
197    fn impl_clone() {
198        // Silly test to mark the #[derive(...)] line as covered.
199        let mut parser = Parser::default();
200
201        let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
202            .unwrap_if_no_warnings();
203        let h2 = h1.clone();
204
205        assert_eq!(h1, h2);
206    }
207
208    #[test]
209    fn only_title() {
210        let mut parser = Parser::default();
211        let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
212            .unwrap_if_no_warnings();
213
214        assert_eq!(
215            mi.item,
216            Header {
217                title_source: Some(Span {
218                    data: "Just the Title",
219                    line: 1,
220                    col: 3,
221                    offset: 2,
222                }),
223                title: Some("Just the Title"),
224                attributes: &[],
225                author_line: None,
226                revision_line: None,
227                comments: &[],
228                source: Span {
229                    data: "= Just the Title",
230                    line: 1,
231                    col: 1,
232                    offset: 0,
233                }
234            }
235        );
236
237        assert_eq!(
238            mi.after,
239            Span {
240                data: "",
241                line: 1,
242                col: 17,
243                offset: 16
244            }
245        );
246    }
247
248    #[test]
249    fn trims_leading_spaces_in_title() {
250        // This is totally a judgement call on my part. As far as I can tell,
251        // the language doesn't describe behavior here.
252        let mut parser = Parser::default();
253        let mi =
254            crate::document::Header::parse(crate::Span::new("=    Just the Title"), &mut parser)
255                .unwrap_if_no_warnings();
256
257        assert_eq!(
258            mi.item,
259            Header {
260                title_source: Some(Span {
261                    data: "Just the Title",
262                    line: 1,
263                    col: 6,
264                    offset: 5,
265                }),
266                title: Some("Just the Title"),
267                attributes: &[],
268                author_line: None,
269                revision_line: None,
270                comments: &[],
271                source: Span {
272                    data: "=    Just the Title",
273                    line: 1,
274                    col: 1,
275                    offset: 0,
276                }
277            }
278        );
279
280        assert_eq!(
281            mi.after,
282            Span {
283                data: "",
284                line: 1,
285                col: 20,
286                offset: 19
287            }
288        );
289    }
290
291    #[test]
292    fn trims_trailing_spaces_in_title() {
293        let mut parser = Parser::default();
294        let mi =
295            crate::document::Header::parse(crate::Span::new("= Just the Title   "), &mut parser)
296                .unwrap_if_no_warnings();
297
298        assert_eq!(
299            mi.item,
300            Header {
301                title_source: Some(Span {
302                    data: "Just the Title",
303                    line: 1,
304                    col: 3,
305                    offset: 2,
306                }),
307                title: Some("Just the Title"),
308                attributes: &[],
309                author_line: None,
310                revision_line: None,
311                comments: &[],
312                source: Span {
313                    data: "= Just the Title",
314                    line: 1,
315                    col: 1,
316                    offset: 0,
317                }
318            }
319        );
320
321        assert_eq!(
322            mi.after,
323            Span {
324                data: "",
325                line: 1,
326                col: 20,
327                offset: 19
328            }
329        );
330    }
331
332    #[test]
333    fn title_and_attribute() {
334        let mut parser = Parser::default();
335
336        let mi = crate::document::Header::parse(
337            crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
338            &mut parser,
339        )
340        .unwrap_if_no_warnings();
341
342        assert_eq!(
343            mi.item,
344            Header {
345                title_source: Some(Span {
346                    data: "Just the Title",
347                    line: 1,
348                    col: 3,
349                    offset: 2,
350                }),
351                title: Some("Just the Title"),
352                attributes: &[Attribute {
353                    name: Span {
354                        data: "foo",
355                        line: 2,
356                        col: 2,
357                        offset: 18,
358                    },
359                    value_source: Some(Span {
360                        data: "bar",
361                        line: 2,
362                        col: 7,
363                        offset: 23,
364                    }),
365                    value: InterpretedValue::Value("bar"),
366                    source: Span {
367                        data: ":foo: bar",
368                        line: 2,
369                        col: 1,
370                        offset: 17,
371                    }
372                }],
373                author_line: None,
374                revision_line: None,
375                comments: &[],
376                source: Span {
377                    data: "= Just the Title\n:foo: bar",
378                    line: 1,
379                    col: 1,
380                    offset: 0,
381                }
382            }
383        );
384
385        assert_eq!(
386            mi.after,
387            Span {
388                data: "blah",
389                line: 4,
390                col: 1,
391                offset: 28
392            }
393        );
394    }
395
396    #[test]
397    fn title_applies_header_substitutions() {
398        let mut parser = Parser::default();
399
400        let mi = crate::document::Header::parse(
401            crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
402            &mut parser,
403        )
404        .unwrap_if_no_warnings();
405
406        assert_eq!(
407            mi.item,
408            Header {
409                title_source: Some(Span {
410                    data: "The Title & Some{sp}Nonsense",
411                    line: 1,
412                    col: 3,
413                    offset: 2,
414                }),
415                title: Some("The Title &amp; Some Nonsense"),
416                attributes: &[Attribute {
417                    name: Span {
418                        data: "foo",
419                        line: 2,
420                        col: 2,
421                        offset: 32,
422                    },
423                    value_source: Some(Span {
424                        data: "bar",
425                        line: 2,
426                        col: 7,
427                        offset: 37,
428                    }),
429                    value: InterpretedValue::Value("bar"),
430                    source: Span {
431                        data: ":foo: bar",
432                        line: 2,
433                        col: 1,
434                        offset: 31,
435                    }
436                }],
437                author_line: None,
438                revision_line: None,
439                comments: &[],
440                source: Span {
441                    data: "= The Title & Some{sp}Nonsense\n:foo: bar",
442                    line: 1,
443                    col: 1,
444                    offset: 0,
445                }
446            }
447        );
448
449        assert_eq!(
450            mi.after,
451            Span {
452                data: "blah",
453                line: 4,
454                col: 1,
455                offset: 42
456            }
457        );
458    }
459
460    #[test]
461    fn attribute_without_title() {
462        let mut parser = Parser::default();
463        let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
464            .unwrap_if_no_warnings();
465
466        assert_eq!(
467            mi.item,
468            Header {
469                title_source: None,
470                title: None,
471                attributes: &[Attribute {
472                    name: Span {
473                        data: "foo",
474                        line: 1,
475                        col: 2,
476                        offset: 1,
477                    },
478                    value_source: Some(Span {
479                        data: "bar",
480                        line: 1,
481                        col: 7,
482                        offset: 6,
483                    }),
484                    value: InterpretedValue::Value("bar"),
485                    source: Span {
486                        data: ":foo: bar",
487                        line: 1,
488                        col: 1,
489                        offset: 0,
490                    }
491                }],
492                author_line: None,
493                revision_line: None,
494                comments: &[],
495                source: Span {
496                    data: ":foo: bar",
497                    line: 1,
498                    col: 1,
499                    offset: 0,
500                }
501            }
502        );
503
504        assert_eq!(
505            mi.after,
506            Span {
507                data: "blah",
508                line: 3,
509                col: 1,
510                offset: 11
511            }
512        );
513    }
514
515    #[test]
516    fn sets_doctitle_attribute() {
517        let mut parser = Parser::default();
518        let _doc = parser.parse("= Document Title Goes Here");
519
520        assert_eq!(
521            parser.attribute_value("doctitle"),
522            InterpretedValue::Value("Document Title Goes Here")
523        );
524    }
525
526    #[test]
527    fn sets_author_attributes_from_author_attribute() {
528        let mut parser = Parser::default();
529        let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
530
531        // Verify that individual author attributes are set.
532        assert_eq!(
533            parser.attribute_value("firstname"),
534            InterpretedValue::Value("John")
535        );
536        assert_eq!(
537            parser.attribute_value("middlename"),
538            InterpretedValue::Value("Q.")
539        );
540        assert_eq!(
541            parser.attribute_value("lastname"),
542            InterpretedValue::Value("Smith")
543        );
544        assert_eq!(
545            parser.attribute_value("authorinitials"),
546            InterpretedValue::Value("JQS")
547        );
548        assert_eq!(
549            parser.attribute_value("email"),
550            InterpretedValue::Value("john@example.com")
551        );
552
553        // Also verify the original author attribute is still set (with HTML encoding).
554        assert_eq!(
555            parser.attribute_value("author"),
556            InterpretedValue::Value("John Q. Smith &lt;john@example.com&gt;")
557        );
558    }
559
560    #[test]
561    fn sets_author_attributes_from_author_attribute_two_names() {
562        let mut parser = Parser::default();
563        let _doc = parser.parse(":author: Jane Doe");
564
565        // Verify that individual author attributes are set.
566        assert_eq!(
567            parser.attribute_value("firstname"),
568            InterpretedValue::Value("Jane")
569        );
570        assert_eq!(
571            parser.attribute_value("middlename"),
572            InterpretedValue::Unset
573        );
574        assert_eq!(
575            parser.attribute_value("lastname"),
576            InterpretedValue::Value("Doe")
577        );
578        assert_eq!(
579            parser.attribute_value("authorinitials"),
580            InterpretedValue::Value("JD")
581        );
582        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
583    }
584
585    #[test]
586    fn sets_author_attributes_from_author_attribute_single_name() {
587        let mut parser = Parser::default();
588        let _doc = parser.parse(":author: Cher");
589
590        // Verify that individual author attributes are set.
591        assert_eq!(
592            parser.attribute_value("firstname"),
593            InterpretedValue::Value("Cher")
594        );
595        assert_eq!(
596            parser.attribute_value("middlename"),
597            InterpretedValue::Unset
598        );
599        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
600        assert_eq!(
601            parser.attribute_value("authorinitials"),
602            InterpretedValue::Value("C")
603        );
604        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
605    }
606
607    #[test]
608    fn sets_author_attributes_from_empty_string() {
609        let mut parser = Parser::default();
610        let _doc = parser.parse(":author:");
611
612        // Verify that individual author attributes are set.
613        assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
614        assert_eq!(
615            parser.attribute_value("middlename"),
616            InterpretedValue::Unset
617        );
618        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
619        assert_eq!(
620            parser.attribute_value("authorinitials"),
621            InterpretedValue::Unset
622        );
623        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
624
625        assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
626    }
627
628    #[test]
629    fn impl_debug() {
630        let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
631        let header = doc.header();
632
633        assert_eq!(
634            format!("{header:#?}"),
635            r#"Header {
636    title_source: Some(
637        Span {
638            data: "Example Title",
639            line: 1,
640            col: 3,
641            offset: 2,
642        },
643    ),
644    title: Some(
645        "Example Title",
646    ),
647    attributes: &[],
648    author_line: None,
649    revision_line: None,
650    comments: &[],
651    source: Span {
652        data: "= Example Title",
653        line: 1,
654        col: 1,
655        offset: 0,
656    },
657}"#
658        );
659    }
660}