1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::{Attrlist, AttrlistContext},
6 content::{Content, SubstitutionGroup},
7 document::{Attribute, Author, AuthorLine, InterpretedValue, RevisionLine},
8 internal::debug::DebugSliceReference,
9 span::MatchedItem,
10 warnings::{MatchAndWarnings, Warning, WarningType},
11};
12
13#[derive(Clone, Eq, PartialEq)]
17pub struct Header<'src> {
18 title_source: Option<Span<'src>>,
19 title: Option<String>,
20 main_title: Option<String>,
21 subtitle: Option<String>,
22 attributes: Vec<Attribute<'src>>,
23 author_line: Option<AuthorLine<'src>>,
24 authors: Vec<Author>,
25 revision_line: Option<RevisionLine<'src>>,
26 comments: Vec<Span<'src>>,
27 source: Span<'src>,
28}
29
30impl<'src> Header<'src> {
31 pub(crate) fn parse(
32 mut source: Span<'src>,
33 parser: &mut Parser,
34 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
35 let original_source = source.discard_empty_lines();
36
37 let mut title_source: Option<Span<'src>> = None;
38 let mut title: Option<String> = None;
39 let mut attributes: Vec<Attribute> = vec![];
40 let mut author_line: Option<AuthorLine<'src>> = None;
41 let mut author_attribute: Option<Author> = None;
42 let mut revision_line: Option<RevisionLine<'src>> = None;
43 let mut comments: Vec<Span<'src>> = vec![];
44 let mut warnings: Vec<Warning<'src>> = vec![];
45
46 while !source.is_empty() {
48 let line_mi = source.take_normalized_line();
49 let line = line_mi.item;
50
51 if line.is_empty() {
53 if title.is_some() {
54 break;
55 }
56 source = line_mi.after;
57 } else if line.starts_with("//") && !line.starts_with("///") {
58 comments.push(line);
59 source = line_mi.after;
60 } else if line.starts_with(':')
61 && let Some(attr) = Attribute::parse(source, parser)
62 {
63 if attr.item.name().data().eq_ignore_ascii_case("author")
66 && let Some(raw_value) = attr.item.raw_value()
67 && let Some(author) = Author::parse(raw_value.data(), parser)
68 {
69 parser.set_attribute_by_value_from_header("firstname", author.firstname());
71 if let Some(middlename) = author.middlename() {
72 parser.set_attribute_by_value_from_header("middlename", middlename);
73 }
74 if let Some(lastname) = author.lastname() {
75 parser.set_attribute_by_value_from_header("lastname", lastname);
76 }
77 parser.set_attribute_by_value_from_header("authorinitials", author.initials());
78 if let Some(email) = author.email() {
79 parser.set_attribute_by_value_from_header("email", email);
80 }
81
82 author_attribute = Some(author);
87 }
88
89 parser.set_attribute_from_header(&attr.item, &mut warnings);
90 attributes.push(attr.item);
91 source = attr.after;
92 } else if title.is_none()
93 && line.starts_with('[')
94 && line.ends_with(']')
95 && line_mi.after.take_normalized_line().item.starts_with("= ")
96 && let Some((separator, separator_warnings)) =
97 parse_separator_attribute(line, parser)
98 {
99 warnings.extend(separator_warnings);
100 parser.set_attribute_by_value_from_header("title-separator", separator);
110 source = line_mi.after;
111 } else if title.is_none() && line.starts_with("= ") {
112 let title_span = line.discard(2).discard_whitespace();
113 let title_str = apply_header_subs(title_span.data(), parser);
114
115 parser.set_attribute_by_value_from_header("doctitle", &title_str);
116
117 title = Some(title_str);
118 title_source = Some(title_span);
119 source = line_mi.after;
120 } else if title.is_some() && author_line.is_none() {
121 author_line = Some(AuthorLine::parse(line, parser));
122 source = line_mi.after;
123 } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
124 revision_line = Some(RevisionLine::parse(line, parser));
125 source = line_mi.after;
126 } else {
127 if title.is_some() {
128 warnings.push(Warning {
129 source: line,
130 warning: WarningType::DocumentHeaderNotTerminated,
131 origin: None,
132 });
133 }
134 break;
135 }
136 }
137
138 let after = source.discard_empty_lines();
139 let source = original_source.trim_remainder(source);
140
141 let (main_title, subtitle) = match &title {
146 Some(title) => {
147 let (main_title, subtitle) = partition_title(title, parser);
148 (Some(main_title), subtitle)
149 }
150 None => (None, None),
151 };
152
153 let authors = resolve_authors(author_line.as_ref(), author_attribute, parser);
157
158 MatchAndWarnings {
159 item: MatchedItem {
160 item: Self {
161 title_source,
162 title,
163 main_title,
164 subtitle,
165 attributes,
166 author_line,
167 authors,
168 revision_line,
169 comments,
170 source: source.trim_trailing_whitespace(),
171 },
172 after,
173 },
174 warnings,
175 }
176 }
177
178 pub fn title_source(&'src self) -> Option<Span<'src>> {
180 self.title_source
181 }
182
183 pub fn title(&self) -> Option<&str> {
193 self.title.as_deref()
194 }
195
196 pub fn main_title(&self) -> Option<&str> {
206 self.main_title.as_deref()
207 }
208
209 pub fn subtitle(&self) -> Option<&str> {
216 self.subtitle.as_deref()
217 }
218
219 pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
221 self.attributes.iter()
222 }
223
224 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
226 self.author_line.as_ref()
227 }
228
229 pub fn authors(&self) -> &[Author] {
238 &self.authors
239 }
240
241 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
243 self.revision_line.as_ref()
244 }
245
246 pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
248 self.comments.iter()
249 }
250}
251
252impl<'src> HasSpan<'src> for Header<'src> {
253 fn span(&self) -> Span<'src> {
254 self.source
255 }
256}
257
258fn parse_separator_attribute<'src>(
269 line: Span<'src>,
270 parser: &Parser,
271) -> Option<(String, Vec<Warning<'src>>)> {
272 let inner = line.slice(1..line.len() - 1);
275
276 if inner.is_empty()
280 || inner.starts_with(' ')
281 || inner.starts_with('\t')
282 || (inner.starts_with('[') && inner.ends_with(']'))
283 {
284 return None;
285 }
286
287 let MatchAndWarnings {
288 item: MatchedItem {
289 item: attrlist,
290 after: _,
291 },
292 warnings,
293 } = Attrlist::parse(inner, parser, AttrlistContext::Block);
294
295 let separator = attrlist
296 .named_attribute("separator")
297 .map(|attr| attr.value().to_string())?;
298
299 Some((separator, warnings))
300}
301
302fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
310 let separator = match parser.effective_attribute("title-separator") {
315 Some(av) => match &av.value {
316 InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
317 _ => ":".to_string(),
318 },
319 None => ":".to_string(),
320 };
321
322 let separator = format!("{separator} ");
323
324 match title.rfind(&separator) {
325 Some(index) => {
326 let main_title = title[..index].to_string();
327 let subtitle = title[index + separator.len()..].to_string();
328 (main_title, Some(subtitle))
329 }
330 None => (title.to_string(), None),
331 }
332}
333
334fn resolve_authors(
349 author_line: Option<&AuthorLine>,
350 author_attribute: Option<Author>,
351 parser: &Parser,
352) -> Vec<Author> {
353 if let Some(author_line) = author_line {
354 return author_line.authors().cloned().collect();
355 }
356
357 let value = |name: &str| match parser.attribute_value(name) {
358 InterpretedValue::Value(value) => Some(value),
359 _ => None,
360 };
361
362 if value("author").is_some()
367 && let Some(author) = author_attribute
368 {
369 return vec![author.with_email(value("email"))];
370 }
371
372 let mut authors = vec![];
374 let mut index = 1;
375
376 while let Some(name) = value(&format!("author_{index}")) {
377 if let Some(author) = Author::parse(&name, parser) {
378 authors.push(author.with_email(value(&format!("email_{index}"))));
379 }
380
381 index += 1;
382 }
383
384 authors
385}
386
387fn apply_header_subs(source: &str, parser: &Parser) -> String {
388 let span = Span::new(source);
389
390 let mut content = Content::from(span);
391 SubstitutionGroup::Header.apply(&mut content, parser, None);
392
393 content.rendered().to_string()
394}
395
396impl std::fmt::Debug for Header<'_> {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 f.debug_struct("Header")
399 .field("title_source", &self.title_source)
400 .field("title", &self.title)
401 .field("main_title", &self.main_title)
402 .field("subtitle", &self.subtitle)
403 .field("attributes", &DebugSliceReference(&self.attributes))
404 .field("author_line", &self.author_line)
405 .field("authors", &self.authors)
406 .field("revision_line", &self.revision_line)
407 .field("comments", &DebugSliceReference(&self.comments))
408 .field("source", &self.source)
409 .finish()
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 #![allow(clippy::unwrap_used)]
416
417 use crate::tests::prelude::*;
418
419 #[test]
420 fn impl_clone() {
421 let mut parser = Parser::default();
423
424 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
425 .unwrap_if_no_warnings();
426 let h2 = h1.clone();
427
428 assert_eq!(h1, h2);
429 }
430
431 #[test]
432 fn only_title() {
433 let mut parser = Parser::default();
434 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
435 .unwrap_if_no_warnings();
436
437 assert_eq!(
438 mi.item,
439 Header {
440 title_source: Some(Span {
441 data: "Just the Title",
442 line: 1,
443 col: 3,
444 offset: 2,
445 }),
446 title: Some("Just the Title"),
447 attributes: &[],
448 author_line: None,
449 revision_line: None,
450 comments: &[],
451 source: Span {
452 data: "= Just the Title",
453 line: 1,
454 col: 1,
455 offset: 0,
456 }
457 }
458 );
459
460 assert_eq!(
461 mi.after,
462 Span {
463 data: "",
464 line: 1,
465 col: 17,
466 offset: 16
467 }
468 );
469 }
470
471 #[test]
472 fn trims_leading_spaces_in_title() {
473 let mut parser = Parser::default();
476 let mi =
477 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
478 .unwrap_if_no_warnings();
479
480 assert_eq!(
481 mi.item,
482 Header {
483 title_source: Some(Span {
484 data: "Just the Title",
485 line: 1,
486 col: 6,
487 offset: 5,
488 }),
489 title: Some("Just the Title"),
490 attributes: &[],
491 author_line: None,
492 revision_line: None,
493 comments: &[],
494 source: Span {
495 data: "= Just the Title",
496 line: 1,
497 col: 1,
498 offset: 0,
499 }
500 }
501 );
502
503 assert_eq!(
504 mi.after,
505 Span {
506 data: "",
507 line: 1,
508 col: 20,
509 offset: 19
510 }
511 );
512 }
513
514 #[test]
515 fn trims_trailing_spaces_in_title() {
516 let mut parser = Parser::default();
517 let mi =
518 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
519 .unwrap_if_no_warnings();
520
521 assert_eq!(
522 mi.item,
523 Header {
524 title_source: Some(Span {
525 data: "Just the Title",
526 line: 1,
527 col: 3,
528 offset: 2,
529 }),
530 title: Some("Just the Title"),
531 attributes: &[],
532 author_line: None,
533 revision_line: None,
534 comments: &[],
535 source: Span {
536 data: "= Just the Title",
537 line: 1,
538 col: 1,
539 offset: 0,
540 }
541 }
542 );
543
544 assert_eq!(
545 mi.after,
546 Span {
547 data: "",
548 line: 1,
549 col: 20,
550 offset: 19
551 }
552 );
553 }
554
555 #[test]
556 fn title_and_attribute() {
557 let mut parser = Parser::default();
558
559 let mi = crate::document::Header::parse(
560 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
561 &mut parser,
562 )
563 .unwrap_if_no_warnings();
564
565 assert_eq!(
566 mi.item,
567 Header {
568 title_source: Some(Span {
569 data: "Just the Title",
570 line: 1,
571 col: 3,
572 offset: 2,
573 }),
574 title: Some("Just the Title"),
575 attributes: &[Attribute {
576 name: Span {
577 data: "foo",
578 line: 2,
579 col: 2,
580 offset: 18,
581 },
582 value_source: Some(Span {
583 data: "bar",
584 line: 2,
585 col: 7,
586 offset: 23,
587 }),
588 value: InterpretedValue::Value("bar"),
589 source: Span {
590 data: ":foo: bar",
591 line: 2,
592 col: 1,
593 offset: 17,
594 }
595 }],
596 author_line: None,
597 revision_line: None,
598 comments: &[],
599 source: Span {
600 data: "= Just the Title\n:foo: bar",
601 line: 1,
602 col: 1,
603 offset: 0,
604 }
605 }
606 );
607
608 assert_eq!(
609 mi.after,
610 Span {
611 data: "blah",
612 line: 4,
613 col: 1,
614 offset: 28
615 }
616 );
617 }
618
619 #[test]
620 fn title_applies_header_substitutions() {
621 let mut parser = Parser::default();
622
623 let mi = crate::document::Header::parse(
624 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
625 &mut parser,
626 )
627 .unwrap_if_no_warnings();
628
629 assert_eq!(
630 mi.item,
631 Header {
632 title_source: Some(Span {
633 data: "The Title & Some{sp}Nonsense",
634 line: 1,
635 col: 3,
636 offset: 2,
637 }),
638 title: Some("The Title & Some Nonsense"),
639 attributes: &[Attribute {
640 name: Span {
641 data: "foo",
642 line: 2,
643 col: 2,
644 offset: 32,
645 },
646 value_source: Some(Span {
647 data: "bar",
648 line: 2,
649 col: 7,
650 offset: 37,
651 }),
652 value: InterpretedValue::Value("bar"),
653 source: Span {
654 data: ":foo: bar",
655 line: 2,
656 col: 1,
657 offset: 31,
658 }
659 }],
660 author_line: None,
661 revision_line: None,
662 comments: &[],
663 source: Span {
664 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
665 line: 1,
666 col: 1,
667 offset: 0,
668 }
669 }
670 );
671
672 assert_eq!(
673 mi.after,
674 Span {
675 data: "blah",
676 line: 4,
677 col: 1,
678 offset: 42
679 }
680 );
681 }
682
683 #[test]
684 fn attribute_without_title() {
685 let mut parser = Parser::default();
686 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
687 .unwrap_if_no_warnings();
688
689 assert_eq!(
690 mi.item,
691 Header {
692 title_source: None,
693 title: None,
694 attributes: &[Attribute {
695 name: Span {
696 data: "foo",
697 line: 1,
698 col: 2,
699 offset: 1,
700 },
701 value_source: Some(Span {
702 data: "bar",
703 line: 1,
704 col: 7,
705 offset: 6,
706 }),
707 value: InterpretedValue::Value("bar"),
708 source: Span {
709 data: ":foo: bar",
710 line: 1,
711 col: 1,
712 offset: 0,
713 }
714 }],
715 author_line: None,
716 revision_line: None,
717 comments: &[],
718 source: Span {
719 data: ":foo: bar",
720 line: 1,
721 col: 1,
722 offset: 0,
723 }
724 }
725 );
726
727 assert_eq!(
728 mi.after,
729 Span {
730 data: "blah",
731 line: 3,
732 col: 1,
733 offset: 11
734 }
735 );
736 }
737
738 #[test]
739 fn sets_doctitle_attribute() {
740 let mut parser = Parser::default();
741 let _doc = parser.parse("= Document Title Goes Here");
742
743 assert_eq!(
744 parser.attribute_value("doctitle"),
745 InterpretedValue::Value("Document Title Goes Here")
746 );
747 }
748
749 #[test]
750 fn sets_author_attributes_from_author_attribute() {
751 let mut parser = Parser::default();
752 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
753
754 assert_eq!(
756 parser.attribute_value("firstname"),
757 InterpretedValue::Value("John")
758 );
759 assert_eq!(
760 parser.attribute_value("middlename"),
761 InterpretedValue::Value("Q.")
762 );
763 assert_eq!(
764 parser.attribute_value("lastname"),
765 InterpretedValue::Value("Smith")
766 );
767 assert_eq!(
768 parser.attribute_value("authorinitials"),
769 InterpretedValue::Value("JQS")
770 );
771 assert_eq!(
772 parser.attribute_value("email"),
773 InterpretedValue::Value("john@example.com")
774 );
775
776 assert_eq!(
778 parser.attribute_value("author"),
779 InterpretedValue::Value("John Q. Smith <john@example.com>")
780 );
781 }
782
783 #[test]
784 fn sets_author_attributes_from_author_attribute_two_names() {
785 let mut parser = Parser::default();
786 let _doc = parser.parse(":author: Jane Doe");
787
788 assert_eq!(
790 parser.attribute_value("firstname"),
791 InterpretedValue::Value("Jane")
792 );
793 assert_eq!(
794 parser.attribute_value("middlename"),
795 InterpretedValue::Unset
796 );
797 assert_eq!(
798 parser.attribute_value("lastname"),
799 InterpretedValue::Value("Doe")
800 );
801 assert_eq!(
802 parser.attribute_value("authorinitials"),
803 InterpretedValue::Value("JD")
804 );
805 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
806 }
807
808 #[test]
809 fn sets_author_attributes_from_author_attribute_single_name() {
810 let mut parser = Parser::default();
811 let _doc = parser.parse(":author: Cher");
812
813 assert_eq!(
815 parser.attribute_value("firstname"),
816 InterpretedValue::Value("Cher")
817 );
818 assert_eq!(
819 parser.attribute_value("middlename"),
820 InterpretedValue::Unset
821 );
822 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
823 assert_eq!(
824 parser.attribute_value("authorinitials"),
825 InterpretedValue::Value("C")
826 );
827 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
828 }
829
830 #[test]
831 fn sets_author_attributes_from_empty_string() {
832 let mut parser = Parser::default();
833 let _doc = parser.parse(":author:");
834
835 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
837 assert_eq!(
838 parser.attribute_value("middlename"),
839 InterpretedValue::Unset
840 );
841 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
842 assert_eq!(
843 parser.attribute_value("authorinitials"),
844 InterpretedValue::Unset
845 );
846 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
847
848 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
849 }
850
851 #[test]
852 fn authors_from_author_line() {
853 let doc = Parser::default().parse("= Title\nKismet R. Lee <kismet@asciidoctor.org>");
854
855 assert_eq!(doc.authors().len(), 1);
856
857 let author = doc.authors().first().unwrap();
858 assert_eq!(author.name(), "Kismet R. Lee");
859 assert_eq!(author.email(), Some("kismet@asciidoctor.org"));
860 assert_eq!(author.initials(), "KRL");
861 }
862
863 #[test]
864 fn authors_from_author_attribute() {
865 let doc =
868 Parser::default().parse("= Title\n:author: Jane Q. Public\n:email: jane@example.com");
869
870 assert_eq!(doc.authors().len(), 1);
871
872 let author = doc.authors().first().unwrap();
873 assert_eq!(author.name(), "Jane Q. Public");
874 assert_eq!(author.firstname(), "Jane");
875 assert_eq!(author.middlename(), Some("Q."));
876 assert_eq!(author.lastname(), Some("Public"));
877 assert_eq!(author.email(), Some("jane@example.com"));
878 assert_eq!(author.initials(), "JQP");
879 }
880
881 #[test]
882 fn authors_from_author_attribute_with_inline_email() {
883 let doc = Parser::default().parse("= Title\n:author: John Q. Smith <john@example.com>");
886
887 assert_eq!(doc.authors().len(), 1);
888
889 let author = doc.authors().first().unwrap();
890 assert_eq!(author.name(), "John Q. Smith");
891 assert_eq!(author.firstname(), "John");
892 assert_eq!(author.middlename(), Some("Q."));
893 assert_eq!(author.lastname(), Some("Smith"));
894 assert_eq!(author.email(), Some("john@example.com"));
895 assert_eq!(author.initials(), "JQS");
896 }
897
898 #[test]
899 fn authors_is_empty_without_author_info() {
900 let doc = Parser::default().parse("= Title\n\nBody.");
901
902 assert!(doc.authors().is_empty());
903 }
904
905 #[test]
906 fn author_unset_after_being_assigned_yields_no_authors() {
907 let doc = Parser::default().parse("= Title\n:author: Jane Doe\n:author!:\n\nBody.");
910
911 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
912 assert!(doc.authors().is_empty());
913 }
914
915 #[test]
916 fn impl_debug() {
917 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
918 let header = doc.header();
919
920 assert_eq!(
921 format!("{header:#?}"),
922 r#"Header {
923 title_source: Some(
924 Span {
925 data: "Example Title",
926 line: 1,
927 col: 3,
928 offset: 2,
929 },
930 ),
931 title: Some(
932 "Example Title",
933 ),
934 main_title: Some(
935 "Example Title",
936 ),
937 subtitle: None,
938 attributes: &[],
939 author_line: None,
940 authors: [],
941 revision_line: None,
942 comments: &[],
943 source: Span {
944 data: "= Example Title",
945 line: 1,
946 col: 1,
947 offset: 0,
948 },
949}"#
950 );
951 }
952
953 #[test]
954 fn no_subtitle() {
955 let doc = Parser::default().parse("= Just the Title");
958 let header = doc.header();
959
960 assert_eq!(header.title(), Some("Just the Title"));
961 assert_eq!(header.main_title(), Some("Just the Title"));
962 assert_eq!(header.subtitle(), None);
963 }
964
965 #[test]
966 fn no_title() {
967 let doc = Parser::default().parse(":foo: bar\n\nbody");
969 let header = doc.header();
970
971 assert_eq!(header.title(), None);
972 assert_eq!(header.main_title(), None);
973 assert_eq!(header.subtitle(), None);
974 }
975
976 #[test]
977 fn colon_without_space_is_not_a_separator() {
978 let doc = Parser::default().parse("= Ratio 3:1 Explained");
981 let header = doc.header();
982
983 assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
984 assert_eq!(header.subtitle(), None);
985 }
986
987 #[test]
988 fn subtitle_available_on_document() {
989 let doc = Parser::default().parse("= Main Title: Subtitle");
992
993 assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
994 assert_eq!(doc.subtitle(), Some("Subtitle"));
995 }
996
997 #[test]
998 fn separator_block_attribute_above_title() {
999 let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
1002 let header = doc.header();
1003
1004 assert_eq!(header.main_title(), Some("Main Title"));
1005 assert_eq!(header.subtitle(), Some("Subtitle"));
1006
1007 let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
1010 let header = doc.header();
1011
1012 assert_eq!(header.main_title(), Some("Main: Title"));
1013 assert_eq!(header.subtitle(), Some("Subtitle"));
1014 }
1015
1016 #[test]
1017 fn separator_attribute_entry_overrides_block_attribute() {
1018 let doc = Parser::default()
1021 .parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
1022 let header = doc.header();
1023
1024 assert_eq!(header.main_title(), Some("Main Title"));
1025 assert_eq!(header.subtitle(), Some("Subtitle"));
1026 }
1027
1028 #[test]
1029 fn non_separator_block_attribute_terminates_header() {
1030 let doc = Parser::default().parse("[foo=bar]\n= Not A Header Title");
1033 let header = doc.header();
1034
1035 assert_eq!(header.title(), None);
1036 assert_eq!(header.subtitle(), None);
1037 }
1038
1039 #[test]
1040 fn bracketed_line_that_is_not_a_separator_attribute_list() {
1041 let doc = Parser::default().parse("[[anchor]]\n= Some Title: Subtitle");
1047 let header = doc.header();
1048
1049 assert_eq!(header.title(), None);
1050 assert_eq!(header.subtitle(), None);
1051
1052 let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
1053 let header = doc.header();
1054
1055 assert_eq!(header.title(), None);
1056 assert_eq!(header.subtitle(), None);
1057 }
1058
1059 #[test]
1060 fn empty_title_separator_falls_back_to_default() {
1061 let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
1064 let header = doc.header();
1065
1066 assert_eq!(header.main_title(), Some("Main Title"));
1067 assert_eq!(header.subtitle(), Some("Subtitle"));
1068 }
1069
1070 #[test]
1071 fn counter_does_not_shadow_title_separator() {
1072 let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
1078 let header = doc.header();
1079
1080 assert_eq!(header.main_title(), Some("Main Title"));
1081 assert_eq!(header.subtitle(), Some("Subtitle 1"));
1082 }
1083}