1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{Attrlist, AttrlistContext},
4 content::{Content, SubstitutionGroup},
5 document::{
6 Attribute, Author, AuthorLine, InterpretedValue, RevisionLine, matches_author_pattern,
7 },
8 internal::{debug::DebugSliceReference, opaque_iter::opaque_slice_iter},
9 span::MatchedItem,
10 warnings::{MatchAndWarnings, Warning, WarningType},
11};
12
13opaque_slice_iter! {
14 pub struct HeaderAttributes<'a> yielding Attribute<'a>;
17}
18
19opaque_slice_iter! {
20 pub struct Comments<'a> yielding Span<'a>;
23}
24
25#[derive(Clone, Eq, PartialEq)]
29pub struct Header<'src> {
30 title_source: Option<Span<'src>>,
31 title: Option<String>,
32 doctitle: Option<String>,
33 main_title: Option<String>,
34 subtitle: Option<String>,
35 id: Option<String>,
36 roles: Vec<String>,
37 attributes: Vec<Attribute<'src>>,
38 author_line: Option<AuthorLine<'src>>,
39 authors: Vec<Author>,
40 revision_line: Option<RevisionLine<'src>>,
41 comments: Vec<Span<'src>>,
42 source: Span<'src>,
43}
44
45impl<'src> Header<'src> {
46 pub(crate) fn parse(
47 mut source: Span<'src>,
48 parser: &mut Parser,
49 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
50 let original_source = source.discard_empty_lines();
51
52 let mut title_source: Option<Span<'src>> = None;
53 let mut title: Option<String> = None;
54
55 let mut saw_implicit_title = false;
61 let mut implicit_overridden_from_above = false;
62 let mut implicit_doctitle_str: Option<String> = None;
63 let mut doctitle_entry_after_title = false;
64
65 let mut id: Option<String> = None;
66 let mut roles: Vec<String> = vec![];
67 let mut attributes: Vec<Attribute> = vec![];
68 let mut author_line: Option<AuthorLine<'src>> = None;
69 let mut author_attribute: Option<Author> = None;
70 let mut authorinitials_from_entry = false;
71 let mut revision_line: Option<RevisionLine<'src>> = None;
72 let mut comments: Vec<Span<'src>> = vec![];
73 let mut warnings: Vec<Warning<'src>> = vec![];
74
75 while !source.is_empty() {
77 let line_mi = source.take_normalized_line();
78 let line = line_mi.item;
79
80 if line.is_empty() {
82 if title.is_some() {
83 break;
84 }
85 source = line_mi.after;
86 } else if line.starts_with("//") && !line.starts_with("///") {
87 comments.push(line);
88 source = line_mi.after;
89 } else if title.is_some()
90 && let Some((after, terminated)) = skip_block_comment(line, line_mi.after)
91 {
92 comments.push(source.trim_remainder(after).trim_trailing_line_end());
104
105 if !terminated {
111 warnings.push(Warning {
112 source: line,
113 warning: WarningType::UnterminatedDelimitedBlock,
114 origin: None,
115 });
116 }
117
118 source = after;
119 } else if line.starts_with(':')
120 && let Some(attr) = Attribute::parse(source, parser)
121 {
122 if attr
130 .item
131 .name()
132 .data()
133 .eq_ignore_ascii_case("authorinitials")
134 {
135 authorinitials_from_entry =
136 !matches!(attr.item.value(), InterpretedValue::Unset);
137 }
138
139 let mut author_name_override: Option<String> = None;
147 if attr.item.name().data().eq_ignore_ascii_case("author")
148 && let Some(raw_value) = attr.item.raw_value()
149 && let Some(author) = Author::parse(raw_value.data(), parser, true)
150 {
151 parser.set_attribute_by_value_from_header("firstname", author.firstname());
153 if let Some(middlename) = author.middlename() {
154 parser.set_attribute_by_value_from_header("middlename", middlename);
155 }
156 if let Some(lastname) = author.lastname() {
157 parser.set_attribute_by_value_from_header("lastname", lastname);
158 }
159
160 if !authorinitials_from_entry {
163 parser.set_attribute_by_value_from_header(
164 "authorinitials",
165 author.initials(),
166 );
167 }
168
169 if let Some(email) = author.email() {
170 parser.set_attribute_by_value_from_header("email", email);
171 }
172
173 let raw = raw_value.data();
182 if !raw.contains('<') && !raw.contains('{') && !matches_author_pattern(raw) {
183 author_name_override = Some(author.name().to_string());
184 }
185
186 author_attribute = Some(author);
191 }
192
193 parser.set_attribute_from_header(&attr.item, &mut warnings);
194
195 if let Some(author_name) = author_name_override {
196 parser.set_attribute_by_value_from_header("author", author_name);
197 }
198
199 if title.is_some() && attr.item.name().data().eq_ignore_ascii_case("doctitle") {
203 doctitle_entry_after_title = true;
204 }
205
206 attributes.push(attr.item);
207 source = attr.after;
208 } else if title.is_none()
209 && line.starts_with('[')
210 && line.ends_with(']')
211 && document_title_follows_block_metadata(line_mi.after)
212 && let Some((metadata, metadata_warnings)) =
213 parse_document_metadata_attrlist(line, parser)
214 {
215 warnings.extend(metadata_warnings);
216
217 if let Some(doc_id) = metadata.id {
235 id = Some(doc_id);
236 }
237 if let Some(separator) = metadata.separator {
238 parser.set_attribute_by_value_from_header("title-separator", separator);
239 }
240 if let Some(reftext) = metadata.reftext {
241 parser.set_attribute_by_value_from_header("reftext", reftext);
242 }
243 if !metadata.roles.is_empty() {
244 roles.extend(metadata.roles);
251 parser.set_attribute_by_value_from_header("role", roles.join(" "));
252 }
253 for option in metadata.options {
254 parser.set_attribute_by_value_from_header(format!("{option}-option"), "");
255 }
256 source = line_mi.after;
257 } else if title.is_none()
258 && let Some(marker) = document_title_marker(line)
259 {
260 let title_span = crate::blocks::strip_symmetric_title_close(
263 line.discard(2).discard_whitespace(),
264 marker,
265 1,
266 );
267 saw_implicit_title = true;
268
269 title_source = Some(title_span);
270
271 if let InterpretedValue::Value(existing) = parser.attribute_value("doctitle")
279 && !existing.is_empty()
280 {
281 implicit_overridden_from_above = true;
282 implicit_doctitle_str = Some(existing.clone());
283 title = Some(existing);
284 } else {
285 let title_str = apply_header_subs(title_span.data(), parser);
286
287 parser.set_attribute_by_value_from_header("doctitle", &title_str);
288
289 implicit_doctitle_str = Some(title_str.clone());
290 title = Some(title_str);
291 }
292
293 source = line_mi.after;
294 } else if title.is_some() && author_line.is_none() {
295 author_line = Some(AuthorLine::parse(line, parser));
296 source = line_mi.after;
297 } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
298 revision_line = Some(RevisionLine::parse(line, parser));
299 source = line_mi.after;
300 } else {
301 if title.is_some() {
302 warnings.push(Warning {
303 source: line,
304 warning: WarningType::DocumentHeaderNotTerminated,
305 origin: None,
306 });
307 }
308 break;
309 }
310 }
311
312 let after = source.discard_empty_lines();
313 let source = original_source.trim_remainder(source);
314
315 let final_doctitle_attr = match parser.attribute_value("doctitle") {
325 InterpretedValue::Value(v) if !v.is_empty() => Some(v),
326 _ => None,
327 };
328
329 title = if saw_implicit_title {
330 let base = if !implicit_overridden_from_above
347 && let Some(raw) = title_source
348 && implicit_doctitle_str
349 .as_deref()
350 .is_some_and(|s| s.contains('{'))
351 {
352 Some(apply_header_subs(raw.data(), parser))
353 } else {
354 title
355 };
356
357 if doctitle_entry_after_title
361 && let Some(ref dt) = final_doctitle_attr
362 && Some(dt) != implicit_doctitle_str.as_ref()
363 {
364 Some(dt.clone())
365 } else {
366 base
367 }
368 } else {
369 final_doctitle_attr
372 };
373
374 let (main_title, subtitle) = match &title {
379 Some(title) => {
380 let (main_title, subtitle) = partition_title(title, parser);
381 (Some(main_title), subtitle)
382 }
383 None => (None, None),
384 };
385
386 let doctitle = match parser.attribute_value("title") {
390 InterpretedValue::Value(v) => Some(v),
391 InterpretedValue::Set => Some(String::new()),
392 InterpretedValue::Unset => title.clone(),
393 };
394
395 let authors = resolve_authors(
401 author_line.as_ref(),
402 author_attribute,
403 !attributes.is_empty(),
404 parser,
405 );
406
407 if !authors.is_empty() {
412 parser.set_attribute_by_value_from_header("authorcount", authors.len().to_string());
413 }
414
415 MatchAndWarnings {
416 item: MatchedItem {
417 item: Self {
418 title_source,
419 title,
420 doctitle,
421 main_title,
422 subtitle,
423 id,
424 roles,
425 attributes,
426 author_line,
427 authors,
428 revision_line,
429 comments,
430 source: source.trim_trailing_whitespace(),
431 },
432 after,
433 },
434 warnings,
435 }
436 }
437
438 pub fn title_source(&'src self) -> Option<Span<'src>> {
440 self.title_source
441 }
442
443 pub fn title(&self) -> Option<&str> {
453 self.title.as_deref()
454 }
455
456 pub(crate) fn doctitle(&self) -> Option<&str> {
468 self.doctitle.as_deref()
469 }
470
471 pub fn main_title(&self) -> Option<&str> {
481 self.main_title.as_deref()
482 }
483
484 pub fn subtitle(&self) -> Option<&str> {
491 self.subtitle.as_deref()
492 }
493
494 pub fn id(&self) -> Option<&str> {
500 self.id.as_deref()
501 }
502
503 pub fn roles(&self) -> Vec<&str> {
512 self.roles.iter().map(String::as_str).collect()
513 }
514
515 pub fn attributes(&'src self) -> HeaderAttributes<'src> {
517 HeaderAttributes::new(&self.attributes)
518 }
519
520 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
522 self.author_line.as_ref()
523 }
524
525 pub fn authors(&self) -> &[Author] {
534 &self.authors
535 }
536
537 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
539 self.revision_line.as_ref()
540 }
541
542 pub fn comments(&'src self) -> Comments<'src> {
544 Comments::new(&self.comments)
545 }
546}
547
548impl<'src> HasSpan<'src> for Header<'src> {
549 fn span(&self) -> Span<'src> {
550 self.source
551 }
552}
553
554fn skip_block_comment<'src>(line: Span<'src>, after: Span<'src>) -> Option<(Span<'src>, bool)> {
569 let delimiter = line.data();
570 if delimiter.len() < 4 || !delimiter.bytes().all(|b| b == b'/') {
571 return None;
572 }
573
574 let mut next = after;
575 let mut terminated = false;
576 while !next.is_empty() {
577 let line_mi = next.take_normalized_line();
578 next = line_mi.after;
579 if line_mi.item.data() == delimiter {
580 terminated = true;
581 break;
582 }
583 }
584
585 Some((next, terminated))
586}
587
588fn document_title_marker(line: Span<'_>) -> Option<char> {
596 if line.starts_with("= ") {
597 Some('=')
598 } else if line.starts_with("# ") {
599 Some('#')
600 } else {
601 None
602 }
603}
604
605fn document_title_follows_block_metadata(after: Span<'_>) -> bool {
621 let mut next = after;
622
623 while !next.is_empty() {
624 let line_mi = next.take_normalized_line();
625 let line = line_mi.item;
626
627 if document_title_marker(line).is_some() {
628 return true;
629 }
630
631 if !is_document_metadata_line(line) {
632 return false;
633 }
634
635 next = line_mi.after;
636 }
637
638 false
639}
640
641fn is_document_metadata_line(line: Span<'_>) -> bool {
653 if !(line.starts_with('[') && line.ends_with(']')) {
654 return false;
655 }
656
657 let inner = line.slice(1..line.len() - 1);
658
659 !(inner.is_empty()
660 || inner.starts_with(' ')
661 || inner.starts_with('\t')
662 || (inner.starts_with('[') && inner.ends_with(']')))
663}
664
665struct DocumentMetadata {
671 id: Option<String>,
672 separator: Option<String>,
673 reftext: Option<String>,
674 roles: Vec<String>,
675 options: Vec<String>,
676}
677
678fn parse_document_metadata_attrlist<'src>(
690 line: Span<'src>,
691 parser: &Parser,
692) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
693 if !is_document_metadata_line(line) {
698 return None;
699 }
700
701 let inner = line.slice(1..line.len() - 1);
703
704 let MatchAndWarnings {
705 item: MatchedItem {
706 item: attrlist,
707 after: _,
708 },
709 warnings,
710 } = Attrlist::parse(inner, parser, AttrlistContext::Block);
711
712 let metadata = DocumentMetadata {
713 id: attrlist.id().map(str::to_string),
714 separator: attrlist
715 .named_attribute("separator")
716 .map(|attr| attr.value().to_string()),
717 reftext: attrlist
718 .named_attribute("reftext")
719 .map(|attr| attr.value().to_string()),
720 roles: attrlist.roles().iter().map(|r| r.to_string()).collect(),
721 options: attrlist.options().iter().map(|o| o.to_string()).collect(),
722 };
723
724 Some((metadata, warnings))
725}
726
727fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
735 let separator = match parser.effective_attribute("title-separator") {
740 Some(av) => match &av.value {
741 InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
742 _ => ":".to_string(),
743 },
744 None => ":".to_string(),
745 };
746
747 let separator = format!("{separator} ");
748
749 match title.rfind(&separator) {
750 Some(index) => {
751 let main_title = title[..index].to_string();
752 let subtitle = title[index + separator.len()..].to_string();
753 (main_title, Some(subtitle))
754 }
755 None => (title.to_string(), None),
756 }
757}
758
759fn resolve_authors(
786 author_line: Option<&AuthorLine>,
787 author_attribute: Option<Author>,
788 header_has_attributes: bool,
789 parser: &mut Parser,
790) -> Vec<Author> {
791 if let Some(author_line) = author_line {
792 return author_line.authors().cloned().collect();
793 }
794
795 if !header_has_attributes {
796 return vec![];
797 }
798
799 if attribute_string(parser, "author").is_some()
806 && let Some(author) = author_attribute
807 {
808 return vec![author.with_email(attribute_string(parser, "email"))];
809 }
810
811 if let Some(authors_value) = attribute_string(parser, "authors") {
814 let authors = collect_indexed_authors(
815 split_author_entries(&authors_value)
816 .into_iter()
817 .filter_map(|entry| Author::parse(entry, parser, true)),
818 parser,
819 );
820
821 if !authors.is_empty() {
822 set_author_metadata(parser, &authors);
823 return authors;
824 }
825 }
826
827 let mut raw_names = vec![];
829 let mut index = 1;
830
831 while let Some(name) = attribute_string(parser, &format!("author_{index}")) {
832 raw_names.push(name);
833 index += 1;
834 }
835
836 let authors = collect_indexed_authors(
837 raw_names
838 .iter()
839 .filter_map(|name| Author::parse(name, parser, true)),
840 parser,
841 );
842
843 if !authors.is_empty() {
844 set_author_metadata(parser, &authors);
845 }
846
847 authors
848}
849
850fn attribute_string(parser: &Parser, name: &str) -> Option<String> {
853 match parser.attribute_value(name) {
854 InterpretedValue::Value(value) => Some(value),
855 _ => None,
856 }
857}
858
859fn collect_indexed_authors(authors: impl Iterator<Item = Author>, parser: &Parser) -> Vec<Author> {
863 authors
864 .enumerate()
865 .map(|(idx, author)| {
866 author.with_email(attribute_string(parser, &format!("email_{}", idx + 1)))
867 })
868 .collect()
869}
870
871fn split_author_entries(value: &str) -> Vec<&str> {
878 let bytes = value.as_bytes();
879 let mut entries: Vec<&str> = Vec::new();
880 let mut start = 0;
881
882 for (index, c) in value.char_indices() {
883 if c != ';' {
884 continue;
885 }
886
887 let is_separator = match bytes.get(index + 1) {
888 Some(next) => *next == b' ',
889 None => true,
890 };
891
892 if is_separator {
893 entries.push(&value[start..index]);
894 start = index + 1;
895 }
896 }
897
898 entries.push(&value[start..]);
899 entries
900}
901
902fn set_author_metadata(parser: &mut Parser, authors: &[Author]) {
919 for (idx, author) in authors.iter().enumerate() {
920 set_author_keys(parser, author, if idx == 0 { None } else { Some(idx + 1) });
921
922 if idx == 1
924 && let Some(first) = authors.first()
925 {
926 set_author_keys(parser, first, Some(1));
927 }
928 }
929
930 let joined = authors
931 .iter()
932 .map(Author::name)
933 .collect::<Vec<_>>()
934 .join(", ");
935
936 parser.set_attribute_by_value_from_header("authors", joined);
937}
938
939fn set_author_keys(parser: &mut Parser, author: &Author, index: Option<usize>) {
942 let key = |name: &str| match index {
943 None => name.to_string(),
944 Some(n) => format!("{name}_{n}"),
945 };
946
947 parser.set_attribute_by_value_from_header(key("author"), author.name());
948 parser.set_attribute_by_value_from_header(key("firstname"), author.firstname());
949
950 if let Some(middlename) = author.middlename() {
951 parser.set_attribute_by_value_from_header(key("middlename"), middlename);
952 }
953
954 if let Some(lastname) = author.lastname() {
955 parser.set_attribute_by_value_from_header(key("lastname"), lastname);
956 }
957
958 parser.set_attribute_by_value_from_header(key("authorinitials"), author.initials());
959
960 if let Some(email) = author.email() {
961 parser.set_attribute_by_value_from_header(key("email"), email);
962 }
963}
964
965fn apply_header_subs(source: &str, parser: &Parser) -> String {
966 let span = Span::new(source);
967
968 let mut content = Content::from(span);
969 SubstitutionGroup::Header.apply(&mut content, parser, None);
970
971 content.rendered().to_string()
972}
973
974impl std::fmt::Debug for Header<'_> {
975 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
976 f.debug_struct("Header")
977 .field("title_source", &self.title_source)
978 .field("title", &self.title)
979 .field("doctitle", &self.doctitle)
980 .field("main_title", &self.main_title)
981 .field("subtitle", &self.subtitle)
982 .field("id", &self.id)
983 .field("roles", &self.roles)
984 .field("attributes", &DebugSliceReference(&self.attributes))
985 .field("author_line", &self.author_line)
986 .field("authors", &self.authors)
987 .field("revision_line", &self.revision_line)
988 .field("comments", &DebugSliceReference(&self.comments))
989 .field("source", &self.source)
990 .finish()
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 #![allow(clippy::unwrap_used)]
997
998 use crate::tests::prelude::*;
999
1000 #[test]
1001 fn attributes_iterator_supports_exact_size_double_ended_and_nth() {
1002 let doc = Parser::default().parse(":alpha: 1\n:bravo: 2\n:charlie: 3\n\nbody\n");
1006 let header = doc.header();
1007
1008 let names: Vec<_> = header
1010 .attributes()
1011 .map(|a| a.name().data().to_string())
1012 .collect();
1013
1014 assert!(names.len() >= 3);
1015 assert_eq!(names.first().map(String::as_str), Some("alpha"));
1016
1017 assert_eq!(header.attributes().len(), names.len());
1018
1019 assert_eq!(
1020 header.attributes().next_back().map(|a| a.name().data()),
1021 names.last().map(String::as_str),
1022 );
1023
1024 assert_eq!(
1025 header.attributes().nth(1).map(|a| a.name().data()),
1026 Some("bravo"),
1027 );
1028 }
1029
1030 #[test]
1031 fn impl_clone() {
1032 let mut parser = Parser::default();
1034
1035 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
1036 .unwrap_if_no_warnings();
1037 let h2 = h1.clone();
1038
1039 assert_eq!(h1, h2);
1040 }
1041
1042 #[test]
1043 fn only_title() {
1044 let mut parser = Parser::default();
1045 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1046 .unwrap_if_no_warnings();
1047
1048 assert_eq!(
1049 mi.item,
1050 Header {
1051 title_source: Some(Span {
1052 data: "Just the Title",
1053 line: 1,
1054 col: 3,
1055 offset: 2,
1056 }),
1057 title: Some("Just the Title"),
1058 attributes: &[],
1059 author_line: None,
1060 revision_line: None,
1061 comments: &[],
1062 source: Span {
1063 data: "= Just the Title",
1064 line: 1,
1065 col: 1,
1066 offset: 0,
1067 }
1068 }
1069 );
1070
1071 assert_eq!(
1072 mi.after,
1073 Span {
1074 data: "",
1075 line: 1,
1076 col: 17,
1077 offset: 16
1078 }
1079 );
1080 }
1081
1082 #[test]
1083 fn trims_leading_spaces_in_title() {
1084 let mut parser = Parser::default();
1087 let mi =
1088 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1089 .unwrap_if_no_warnings();
1090
1091 assert_eq!(
1092 mi.item,
1093 Header {
1094 title_source: Some(Span {
1095 data: "Just the Title",
1096 line: 1,
1097 col: 6,
1098 offset: 5,
1099 }),
1100 title: Some("Just the Title"),
1101 attributes: &[],
1102 author_line: None,
1103 revision_line: None,
1104 comments: &[],
1105 source: Span {
1106 data: "= Just the Title",
1107 line: 1,
1108 col: 1,
1109 offset: 0,
1110 }
1111 }
1112 );
1113
1114 assert_eq!(
1115 mi.after,
1116 Span {
1117 data: "",
1118 line: 1,
1119 col: 20,
1120 offset: 19
1121 }
1122 );
1123 }
1124
1125 #[test]
1126 fn trims_trailing_spaces_in_title() {
1127 let mut parser = Parser::default();
1128 let mi =
1129 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
1130 .unwrap_if_no_warnings();
1131
1132 assert_eq!(
1133 mi.item,
1134 Header {
1135 title_source: Some(Span {
1136 data: "Just the Title",
1137 line: 1,
1138 col: 3,
1139 offset: 2,
1140 }),
1141 title: Some("Just the Title"),
1142 attributes: &[],
1143 author_line: None,
1144 revision_line: None,
1145 comments: &[],
1146 source: Span {
1147 data: "= Just the Title",
1148 line: 1,
1149 col: 1,
1150 offset: 0,
1151 }
1152 }
1153 );
1154
1155 assert_eq!(
1156 mi.after,
1157 Span {
1158 data: "",
1159 line: 1,
1160 col: 20,
1161 offset: 19
1162 }
1163 );
1164 }
1165
1166 #[test]
1167 fn title_and_attribute() {
1168 let mut parser = Parser::default();
1169
1170 let mi = crate::document::Header::parse(
1171 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
1172 &mut parser,
1173 )
1174 .unwrap_if_no_warnings();
1175
1176 assert_eq!(
1177 mi.item,
1178 Header {
1179 title_source: Some(Span {
1180 data: "Just the Title",
1181 line: 1,
1182 col: 3,
1183 offset: 2,
1184 }),
1185 title: Some("Just the Title"),
1186 attributes: &[Attribute {
1187 name: Span {
1188 data: "foo",
1189 line: 2,
1190 col: 2,
1191 offset: 18,
1192 },
1193 value_source: Some(Span {
1194 data: "bar",
1195 line: 2,
1196 col: 7,
1197 offset: 23,
1198 }),
1199 value: InterpretedValue::Value("bar"),
1200 source: Span {
1201 data: ":foo: bar",
1202 line: 2,
1203 col: 1,
1204 offset: 17,
1205 }
1206 }],
1207 author_line: None,
1208 revision_line: None,
1209 comments: &[],
1210 source: Span {
1211 data: "= Just the Title\n:foo: bar",
1212 line: 1,
1213 col: 1,
1214 offset: 0,
1215 }
1216 }
1217 );
1218
1219 assert_eq!(
1220 mi.after,
1221 Span {
1222 data: "blah",
1223 line: 4,
1224 col: 1,
1225 offset: 28
1226 }
1227 );
1228 }
1229
1230 #[test]
1231 fn title_applies_header_substitutions() {
1232 let mut parser = Parser::default();
1233
1234 let mi = crate::document::Header::parse(
1235 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
1236 &mut parser,
1237 )
1238 .unwrap_if_no_warnings();
1239
1240 assert_eq!(
1241 mi.item,
1242 Header {
1243 title_source: Some(Span {
1244 data: "The Title & Some{sp}Nonsense",
1245 line: 1,
1246 col: 3,
1247 offset: 2,
1248 }),
1249 title: Some("The Title & Some Nonsense"),
1250 attributes: &[Attribute {
1251 name: Span {
1252 data: "foo",
1253 line: 2,
1254 col: 2,
1255 offset: 32,
1256 },
1257 value_source: Some(Span {
1258 data: "bar",
1259 line: 2,
1260 col: 7,
1261 offset: 37,
1262 }),
1263 value: InterpretedValue::Value("bar"),
1264 source: Span {
1265 data: ":foo: bar",
1266 line: 2,
1267 col: 1,
1268 offset: 31,
1269 }
1270 }],
1271 author_line: None,
1272 revision_line: None,
1273 comments: &[],
1274 source: Span {
1275 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
1276 line: 1,
1277 col: 1,
1278 offset: 0,
1279 }
1280 }
1281 );
1282
1283 assert_eq!(
1284 mi.after,
1285 Span {
1286 data: "blah",
1287 line: 4,
1288 col: 1,
1289 offset: 42
1290 }
1291 );
1292 }
1293
1294 #[test]
1295 fn attribute_without_title() {
1296 let mut parser = Parser::default();
1297 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
1298 .unwrap_if_no_warnings();
1299
1300 assert_eq!(
1301 mi.item,
1302 Header {
1303 title_source: None,
1304 title: None,
1305 attributes: &[Attribute {
1306 name: Span {
1307 data: "foo",
1308 line: 1,
1309 col: 2,
1310 offset: 1,
1311 },
1312 value_source: Some(Span {
1313 data: "bar",
1314 line: 1,
1315 col: 7,
1316 offset: 6,
1317 }),
1318 value: InterpretedValue::Value("bar"),
1319 source: Span {
1320 data: ":foo: bar",
1321 line: 1,
1322 col: 1,
1323 offset: 0,
1324 }
1325 }],
1326 author_line: None,
1327 revision_line: None,
1328 comments: &[],
1329 source: Span {
1330 data: ":foo: bar",
1331 line: 1,
1332 col: 1,
1333 offset: 0,
1334 }
1335 }
1336 );
1337
1338 assert_eq!(
1339 mi.after,
1340 Span {
1341 data: "blah",
1342 line: 3,
1343 col: 1,
1344 offset: 11
1345 }
1346 );
1347 }
1348
1349 #[test]
1350 fn sets_doctitle_attribute() {
1351 let mut parser = Parser::default();
1352 let _doc = parser.parse("= Document Title Goes Here");
1353
1354 assert_eq!(
1355 parser.attribute_value("doctitle"),
1356 InterpretedValue::Value("Document Title Goes Here")
1357 );
1358 }
1359
1360 #[test]
1361 fn sets_author_attributes_from_author_attribute() {
1362 let mut parser = Parser::default();
1363 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
1364
1365 assert_eq!(
1367 parser.attribute_value("firstname"),
1368 InterpretedValue::Value("John")
1369 );
1370 assert_eq!(
1371 parser.attribute_value("middlename"),
1372 InterpretedValue::Value("Q.")
1373 );
1374 assert_eq!(
1375 parser.attribute_value("lastname"),
1376 InterpretedValue::Value("Smith")
1377 );
1378 assert_eq!(
1379 parser.attribute_value("authorinitials"),
1380 InterpretedValue::Value("JQS")
1381 );
1382 assert_eq!(
1383 parser.attribute_value("email"),
1384 InterpretedValue::Value("john@example.com")
1385 );
1386
1387 assert_eq!(
1389 parser.attribute_value("author"),
1390 InterpretedValue::Value("John Q. Smith <john@example.com>")
1391 );
1392 }
1393
1394 #[test]
1395 fn author_attribute_with_four_or_more_parts_is_partitioned() {
1396 let mut parser = Parser::default();
1402 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr.");
1403
1404 assert_eq!(
1405 parser.attribute_value("author"),
1406 InterpretedValue::Value("Leroy Harold Scherer, Jr.")
1407 );
1408 assert_eq!(
1409 parser.attribute_value("firstname"),
1410 InterpretedValue::Value("Leroy")
1411 );
1412 assert_eq!(
1413 parser.attribute_value("middlename"),
1414 InterpretedValue::Value("Harold")
1415 );
1416 assert_eq!(
1417 parser.attribute_value("lastname"),
1418 InterpretedValue::Value("Scherer, Jr.")
1419 );
1420 assert_eq!(
1421 parser.attribute_value("authorinitials"),
1422 InterpretedValue::Value("LHS")
1423 );
1424 }
1425
1426 #[test]
1427 fn author_attribute_two_part_fallback_partitions_lastname() {
1428 let mut parser = Parser::default();
1432 let _doc = parser.parse(":author: Jane, Doe");
1433
1434 assert_eq!(
1435 parser.attribute_value("author"),
1436 InterpretedValue::Value("Jane, Doe")
1437 );
1438 assert_eq!(
1439 parser.attribute_value("firstname"),
1440 InterpretedValue::Value("Jane,")
1441 );
1442 assert_eq!(
1443 parser.attribute_value("middlename"),
1444 InterpretedValue::Unset
1445 );
1446 assert_eq!(
1447 parser.attribute_value("lastname"),
1448 InterpretedValue::Value("Doe")
1449 );
1450 }
1451
1452 #[test]
1453 fn author_attribute_single_part_fallback_is_firstname_only() {
1454 let mut parser = Parser::default();
1457 let _doc = parser.parse(":author: Jane,");
1458
1459 assert_eq!(
1460 parser.attribute_value("author"),
1461 InterpretedValue::Value("Jane,")
1462 );
1463 assert_eq!(
1464 parser.attribute_value("firstname"),
1465 InterpretedValue::Value("Jane,")
1466 );
1467 assert_eq!(
1468 parser.attribute_value("middlename"),
1469 InterpretedValue::Unset
1470 );
1471 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1472 }
1473
1474 #[test]
1475 fn author_attribute_four_or_more_parts_with_inline_email() {
1476 let mut parser = Parser::default();
1480 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr. <leroy@example.com>");
1481
1482 assert_eq!(
1483 parser.attribute_value("firstname"),
1484 InterpretedValue::Value("Leroy")
1485 );
1486 assert_eq!(
1487 parser.attribute_value("middlename"),
1488 InterpretedValue::Value("Harold")
1489 );
1490 assert_eq!(
1491 parser.attribute_value("lastname"),
1492 InterpretedValue::Value("Scherer, Jr.")
1493 );
1494 assert_eq!(
1495 parser.attribute_value("email"),
1496 InterpretedValue::Value("leroy@example.com")
1497 );
1498 assert_eq!(
1499 parser.attribute_value("authorinitials"),
1500 InterpretedValue::Value("LHS")
1501 );
1502 }
1503
1504 #[test]
1505 fn author_attribute_reference_expands_and_partitions() {
1506 let mut parser = Parser::default();
1510 let _doc = parser.parse(":full-name: Leroy Harold Scherer, Jr.\n:author: {full-name}");
1511
1512 assert_eq!(
1513 parser.attribute_value("firstname"),
1514 InterpretedValue::Value("Leroy")
1515 );
1516 assert_eq!(
1517 parser.attribute_value("middlename"),
1518 InterpretedValue::Value("Harold")
1519 );
1520 assert_eq!(
1521 parser.attribute_value("lastname"),
1522 InterpretedValue::Value("Scherer, Jr.")
1523 );
1524 assert_eq!(
1525 parser.attribute_value("authorinitials"),
1526 InterpretedValue::Value("LHS")
1527 );
1528 }
1529
1530 #[test]
1531 fn author_attribute_reference_within_larger_value_expands_and_partitions() {
1532 let mut parser = Parser::default();
1536 let _doc = parser.parse(":rest: Harold Scherer, Jr.\n:author: Leroy {rest}");
1537
1538 assert_eq!(
1539 parser.attribute_value("firstname"),
1540 InterpretedValue::Value("Leroy")
1541 );
1542 assert_eq!(
1543 parser.attribute_value("middlename"),
1544 InterpretedValue::Value("Harold")
1545 );
1546 assert_eq!(
1547 parser.attribute_value("lastname"),
1548 InterpretedValue::Value("Scherer, Jr.")
1549 );
1550 }
1551
1552 #[test]
1553 fn author_attribute_non_breaking_space_is_not_a_name_separator() {
1554 let mut parser = Parser::default();
1558 let _doc = parser.parse(":author: John\u{a0}Doe Scherer, Jr.");
1559
1560 assert_eq!(
1561 parser.attribute_value("firstname"),
1562 InterpretedValue::Value("John\u{a0}Doe")
1563 );
1564 assert_eq!(
1565 parser.attribute_value("middlename"),
1566 InterpretedValue::Value("Scherer,")
1567 );
1568 assert_eq!(
1569 parser.attribute_value("lastname"),
1570 InterpretedValue::Value("Jr.")
1571 );
1572 }
1573
1574 #[test]
1575 fn sets_author_attributes_from_author_attribute_two_names() {
1576 let mut parser = Parser::default();
1577 let _doc = parser.parse(":author: Jane Doe");
1578
1579 assert_eq!(
1581 parser.attribute_value("firstname"),
1582 InterpretedValue::Value("Jane")
1583 );
1584 assert_eq!(
1585 parser.attribute_value("middlename"),
1586 InterpretedValue::Unset
1587 );
1588 assert_eq!(
1589 parser.attribute_value("lastname"),
1590 InterpretedValue::Value("Doe")
1591 );
1592 assert_eq!(
1593 parser.attribute_value("authorinitials"),
1594 InterpretedValue::Value("JD")
1595 );
1596 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1597 }
1598
1599 #[test]
1600 fn sets_author_attributes_from_author_attribute_single_name() {
1601 let mut parser = Parser::default();
1602 let _doc = parser.parse(":author: Cher");
1603
1604 assert_eq!(
1606 parser.attribute_value("firstname"),
1607 InterpretedValue::Value("Cher")
1608 );
1609 assert_eq!(
1610 parser.attribute_value("middlename"),
1611 InterpretedValue::Unset
1612 );
1613 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1614 assert_eq!(
1615 parser.attribute_value("authorinitials"),
1616 InterpretedValue::Value("C")
1617 );
1618 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1619 }
1620
1621 #[test]
1622 fn sets_author_attributes_from_empty_string() {
1623 let mut parser = Parser::default();
1624 let _doc = parser.parse(":author:");
1625
1626 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
1628 assert_eq!(
1629 parser.attribute_value("middlename"),
1630 InterpretedValue::Unset
1631 );
1632 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1633 assert_eq!(
1634 parser.attribute_value("authorinitials"),
1635 InterpretedValue::Unset
1636 );
1637 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1638
1639 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
1640 }
1641
1642 #[test]
1643 fn authors_from_author_line() {
1644 let doc = Parser::default().parse("= Title\nKismet R. Lee <kismet@asciidoctor.org>");
1645
1646 assert_eq!(doc.authors().len(), 1);
1647
1648 let author = doc.authors().first().unwrap();
1649 assert_eq!(author.name(), "Kismet R. Lee");
1650 assert_eq!(author.email(), Some("kismet@asciidoctor.org"));
1651 assert_eq!(author.initials(), "KRL");
1652 }
1653
1654 #[test]
1655 fn authors_from_author_attribute() {
1656 let doc =
1659 Parser::default().parse("= Title\n:author: Jane Q. Public\n:email: jane@example.com");
1660
1661 assert_eq!(doc.authors().len(), 1);
1662
1663 let author = doc.authors().first().unwrap();
1664 assert_eq!(author.name(), "Jane Q. Public");
1665 assert_eq!(author.firstname(), "Jane");
1666 assert_eq!(author.middlename(), Some("Q."));
1667 assert_eq!(author.lastname(), Some("Public"));
1668 assert_eq!(author.email(), Some("jane@example.com"));
1669 assert_eq!(author.initials(), "JQP");
1670 }
1671
1672 #[test]
1673 fn authors_from_author_attribute_with_inline_email() {
1674 let doc = Parser::default().parse("= Title\n:author: John Q. Smith <john@example.com>");
1677
1678 assert_eq!(doc.authors().len(), 1);
1679
1680 let author = doc.authors().first().unwrap();
1681 assert_eq!(author.name(), "John Q. Smith");
1682 assert_eq!(author.firstname(), "John");
1683 assert_eq!(author.middlename(), Some("Q."));
1684 assert_eq!(author.lastname(), Some("Smith"));
1685 assert_eq!(author.email(), Some("john@example.com"));
1686 assert_eq!(author.initials(), "JQS");
1687 }
1688
1689 #[test]
1690 fn authors_is_empty_without_author_info() {
1691 let doc = Parser::default().parse("= Title\n\nBody.");
1692
1693 assert!(doc.authors().is_empty());
1694 }
1695
1696 #[test]
1697 fn authorcount_reflects_author_line() {
1698 let doc = Parser::default().parse("= Title\nJane Doe; John Smith\n\nBody.");
1701
1702 assert_eq!(doc.authors().len(), 2);
1703 assert_eq!(
1704 doc.attribute_value("authorcount"),
1705 InterpretedValue::Value("2")
1706 );
1707
1708 let doc = Parser::default().parse(":author: Jane Doe\n\nBody.");
1710
1711 assert_eq!(
1712 doc.attribute_value("authorcount"),
1713 InterpretedValue::Value("1")
1714 );
1715
1716 let doc = Parser::default().parse("= Title\n\nBody.");
1718
1719 assert_eq!(
1720 doc.attribute_value("authorcount"),
1721 InterpretedValue::Value("0")
1722 );
1723 }
1724
1725 #[test]
1726 fn explicit_authorinitials_after_author_still_wins() {
1727 let doc = Parser::default().parse(":author: Doc Writer\n:authorinitials: DOC\n\nBody.");
1730
1731 assert_eq!(
1732 doc.attribute_value("authorinitials"),
1733 InterpretedValue::Value("DOC")
1734 );
1735
1736 let doc = Parser::default()
1739 .parse(":author: Jane Roe\n:authorinitials: DOC\n:author: Doc Writer\n\nBody.");
1740
1741 assert_eq!(
1742 doc.attribute_value("author"),
1743 InterpretedValue::Value("Doc Writer")
1744 );
1745 assert_eq!(
1746 doc.attribute_value("authorinitials"),
1747 InterpretedValue::Value("DOC")
1748 );
1749 }
1750
1751 #[test]
1752 fn later_author_entry_redrives_initials_without_explicit_override() {
1753 let doc = Parser::default().parse(":author: Jane Roe\n:author: Doc Writer\n\nBody.");
1756
1757 assert_eq!(
1758 doc.attribute_value("authorinitials"),
1759 InterpretedValue::Value("DW")
1760 );
1761 }
1762
1763 #[test]
1764 fn explicit_authorinitials_not_preserved_for_indexed_or_authors_forms() {
1765 let doc = Parser::default().parse(":authorinitials: DOC\n:author_1: Doc Writer\n\nBody.");
1770
1771 assert_eq!(
1772 doc.attribute_value("author"),
1773 InterpretedValue::Value("Doc Writer")
1774 );
1775 assert_eq!(
1776 doc.attribute_value("authorinitials"),
1777 InterpretedValue::Value("DW")
1778 );
1779 }
1780
1781 #[test]
1782 fn authors_attribute_splits_into_indexed_authors() {
1783 let doc = Parser::default().parse(":authors: Jane Doe; John Q. Smith\n\nBody.");
1786
1787 assert_eq!(doc.authors().len(), 2);
1788 assert_eq!(
1789 doc.attribute_value("authors"),
1790 InterpretedValue::Value("Jane Doe, John Q. Smith")
1791 );
1792 assert_eq!(
1793 doc.attribute_value("author"),
1794 InterpretedValue::Value("Jane Doe")
1795 );
1796 assert_eq!(
1797 doc.attribute_value("author_2"),
1798 InterpretedValue::Value("John Q. Smith")
1799 );
1800 assert_eq!(
1801 doc.attribute_value("middlename_2"),
1802 InterpretedValue::Value("Q.")
1803 );
1804 assert_eq!(
1805 doc.attribute_value("authorinitials_2"),
1806 InterpretedValue::Value("JQS")
1807 );
1808 }
1809
1810 #[test]
1811 fn authors_attribute_attaches_companion_emails_and_base_middlename() {
1812 let doc = Parser::default().parse(
1816 ":authors: Jane Q. Doe; John Smith\n:email_1: jane@example.com\n:email_2: john@example.com\n\nBody.",
1817 );
1818
1819 let authors = doc.authors();
1820 assert_eq!(authors.len(), 2);
1821 assert_eq!(authors.first().unwrap().email(), Some("jane@example.com"));
1822 assert_eq!(authors.get(1).unwrap().email(), Some("john@example.com"));
1823
1824 assert_eq!(
1825 doc.attribute_value("middlename"),
1826 InterpretedValue::Value("Q.")
1827 );
1828 assert_eq!(
1829 doc.attribute_value("email"),
1830 InterpretedValue::Value("jane@example.com")
1831 );
1832 assert_eq!(
1833 doc.attribute_value("email_2"),
1834 InterpretedValue::Value("john@example.com")
1835 );
1836 }
1837
1838 #[test]
1839 fn authors_attribute_semicolon_without_space_is_one_author() {
1840 let doc = Parser::default().parse(":authors: Joe Doe;Smith Johnson\n\nBody.");
1843
1844 assert_eq!(doc.authors().len(), 1);
1845 assert_eq!(
1846 doc.attribute_value("authorcount"),
1847 InterpretedValue::Value("1")
1848 );
1849 }
1850
1851 #[test]
1852 fn authors_attribute_single_name_authors_and_trailing_separator() {
1853 let doc = Parser::default().parse(":authors: Cher; Madonna;\n\nBody.");
1856
1857 assert_eq!(doc.authors().len(), 2);
1858 assert_eq!(
1859 doc.attribute_value("authors"),
1860 InterpretedValue::Value("Cher, Madonna")
1861 );
1862 assert_eq!(
1863 doc.attribute_value("author"),
1864 InterpretedValue::Value("Cher")
1865 );
1866 assert_eq!(doc.attribute_value("lastname"), InterpretedValue::Unset);
1867 assert_eq!(
1868 doc.attribute_value("authorinitials"),
1869 InterpretedValue::Value("C")
1870 );
1871 assert_eq!(
1872 doc.attribute_value("author_2"),
1873 InterpretedValue::Value("Madonna")
1874 );
1875 assert_eq!(doc.attribute_value("lastname_2"), InterpretedValue::Unset);
1876 assert_eq!(
1877 doc.attribute_value("authorcount"),
1878 InterpretedValue::Value("2")
1879 );
1880 }
1881
1882 #[test]
1883 fn authors_attribute_with_only_empty_entries_yields_no_authors() {
1884 let doc = Parser::default().parse(":authors: ;\n\nBody.");
1887
1888 assert!(doc.authors().is_empty());
1889 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
1890 assert_eq!(
1891 doc.attribute_value("authorcount"),
1892 InterpretedValue::Value("0")
1893 );
1894
1895 assert_eq!(doc.attribute_value("authors"), InterpretedValue::Value(";"));
1899 }
1900
1901 #[test]
1902 fn author_attribute_takes_precedence_over_authors() {
1903 let doc = Parser::default()
1907 .parse(":author: Solo Writer\n:authors: Jane Doe; John Smith\n\nBody.");
1908
1909 assert_eq!(doc.authors().len(), 1);
1910 assert_eq!(
1911 doc.attribute_value("author"),
1912 InterpretedValue::Value("Solo Writer")
1913 );
1914 assert_eq!(doc.attribute_value("author_2"), InterpretedValue::Unset);
1915 }
1916
1917 #[test]
1918 fn author_unset_after_being_assigned_yields_no_authors() {
1919 let doc = Parser::default().parse("= Title\n:author: Jane Doe\n:author!:\n\nBody.");
1922
1923 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
1924 assert!(doc.authors().is_empty());
1925 }
1926
1927 #[test]
1928 fn impl_debug() {
1929 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
1930 let header = doc.header();
1931
1932 assert_eq!(
1933 format!("{header:#?}"),
1934 r#"Header {
1935 title_source: Some(
1936 Span {
1937 data: "Example Title",
1938 line: 1,
1939 col: 3,
1940 offset: 2,
1941 },
1942 ),
1943 title: Some(
1944 "Example Title",
1945 ),
1946 doctitle: Some(
1947 "Example Title",
1948 ),
1949 main_title: Some(
1950 "Example Title",
1951 ),
1952 subtitle: None,
1953 id: None,
1954 roles: [],
1955 attributes: &[],
1956 author_line: None,
1957 authors: [],
1958 revision_line: None,
1959 comments: &[],
1960 source: Span {
1961 data: "= Example Title",
1962 line: 1,
1963 col: 1,
1964 offset: 0,
1965 },
1966}"#
1967 );
1968 }
1969
1970 #[test]
1971 fn no_subtitle() {
1972 let doc = Parser::default().parse("= Just the Title");
1975 let header = doc.header();
1976
1977 assert_eq!(header.title(), Some("Just the Title"));
1978 assert_eq!(header.main_title(), Some("Just the Title"));
1979 assert_eq!(header.subtitle(), None);
1980 }
1981
1982 #[test]
1983 fn no_title() {
1984 let doc = Parser::default().parse(":foo: bar\n\nbody");
1986 let header = doc.header();
1987
1988 assert_eq!(header.title(), None);
1989 assert_eq!(header.main_title(), None);
1990 assert_eq!(header.subtitle(), None);
1991 }
1992
1993 #[test]
1994 fn colon_without_space_is_not_a_separator() {
1995 let doc = Parser::default().parse("= Ratio 3:1 Explained");
1998 let header = doc.header();
1999
2000 assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
2001 assert_eq!(header.subtitle(), None);
2002 }
2003
2004 #[test]
2005 fn subtitle_available_on_document() {
2006 let doc = Parser::default().parse("= Main Title: Subtitle");
2009
2010 assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
2011 assert_eq!(doc.subtitle(), Some("Subtitle"));
2012 }
2013
2014 #[test]
2015 fn separator_block_attribute_above_title() {
2016 let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
2019 let header = doc.header();
2020
2021 assert_eq!(header.main_title(), Some("Main Title"));
2022 assert_eq!(header.subtitle(), Some("Subtitle"));
2023
2024 let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
2027 let header = doc.header();
2028
2029 assert_eq!(header.main_title(), Some("Main: Title"));
2030 assert_eq!(header.subtitle(), Some("Subtitle"));
2031 }
2032
2033 #[test]
2034 fn separator_attribute_entry_overrides_block_attribute() {
2035 let doc = Parser::default()
2038 .parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
2039 let header = doc.header();
2040
2041 assert_eq!(header.main_title(), Some("Main Title"));
2042 assert_eq!(header.subtitle(), Some("Subtitle"));
2043 }
2044
2045 #[test]
2046 fn unrecognized_block_attribute_above_title_is_consumed() {
2047 let doc = Parser::default().parse("[foo=bar]\n= A Header Title");
2053 let header = doc.header();
2054
2055 assert_eq!(header.title(), Some("A Header Title"));
2056 assert_eq!(header.subtitle(), None);
2057 assert_eq!(doc.attribute_value("foo"), InterpretedValue::Unset);
2058 }
2059
2060 #[test]
2061 fn reftext_block_attribute_above_title() {
2062 let doc =
2066 Parser::default().parse("[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2067 let header = doc.header();
2068
2069 assert_eq!(header.title(), Some("Links & Stuff"));
2070 assert_eq!(
2071 doc.attribute_value("reftext"),
2072 InterpretedValue::Value("Links and Stuff")
2073 );
2074 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2075 }
2076
2077 #[test]
2078 fn id_block_attribute_above_title() {
2079 let doc = Parser::default().parse("[#docid]\n= Document Title\n\nBody.");
2082 let header = doc.header();
2083
2084 assert_eq!(header.title(), Some("Document Title"));
2085 assert_eq!(header.id(), Some("docid"));
2086 assert_eq!(doc.id(), Some("docid"));
2087
2088 let doc = Parser::default().parse("[id=docid]\n= Document Title");
2090 assert_eq!(doc.header().id(), Some("docid"));
2091 }
2092
2093 #[test]
2094 fn stacked_block_attributes_above_title() {
2095 let doc = Parser::default()
2100 .parse("[#docid]\n[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2101 let header = doc.header();
2102
2103 assert_eq!(header.title(), Some("Links & Stuff"));
2104 assert_eq!(header.id(), Some("docid"));
2105 assert_eq!(doc.id(), Some("docid"));
2106 assert_eq!(
2107 doc.attribute_value("reftext"),
2108 InterpretedValue::Value("Links and Stuff")
2109 );
2110 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2111 }
2112
2113 #[test]
2114 fn stacked_block_attributes_combine_roles() {
2115 let doc = Parser::default().parse("[#docid]\n[.one]\n[.two]\n= Document Title");
2119 let header = doc.header();
2120
2121 assert_eq!(header.title(), Some("Document Title"));
2122 assert_eq!(header.id(), Some("docid"));
2123 assert_eq!(
2124 doc.attribute_value("role"),
2125 InterpretedValue::Value("one two")
2126 );
2127 assert_eq!(header.roles(), vec!["one", "two"]);
2128 }
2129
2130 #[test]
2131 fn stacked_block_attributes_require_a_following_title() {
2132 let doc = Parser::default().parse("[#docid]\n[reftext=\"Stuff\"]\n\nBody.");
2136 let header = doc.header();
2137
2138 assert_eq!(header.title(), None);
2139 assert_eq!(header.id(), None);
2140 assert_eq!(doc.attribute_value("reftext"), InterpretedValue::Unset);
2141 }
2142
2143 #[test]
2144 fn stacked_block_attributes_stop_at_a_block_anchor() {
2145 let doc = Parser::default().parse("[#docid]\n[[anchor]]\n= Some Title");
2150 let header = doc.header();
2151
2152 assert_eq!(header.title(), None);
2153 assert_eq!(header.id(), None);
2154 }
2155
2156 #[test]
2157 fn rejected_metadata_run_does_not_fire_counter() {
2158 let doc = Parser::default()
2171 .parse("[reftext=\"See {counter:item}\"]\n[[anchor]]\n= Title\n\n{counter:item}");
2172
2173 assert_eq!(doc.header().title(), None);
2174 assert_eq!(rendered_paragraphs(&doc), vec!["= Title", "2"]);
2175 }
2176
2177 #[test]
2178 fn role_block_attribute_above_title() {
2179 let doc = Parser::default().parse("[role=special]\n= Document Title\n\nBody.");
2184 let header = doc.header();
2185
2186 assert_eq!(header.title(), Some("Document Title"));
2187 assert_eq!(
2188 doc.attribute_value("role"),
2189 InterpretedValue::Value("special")
2190 );
2191 assert_eq!(header.roles(), vec!["special"]);
2192 assert_eq!(doc.roles(), vec!["special"]);
2193
2194 let doc = Parser::default().parse("[.one.two]\n= Document Title");
2196 assert_eq!(
2197 doc.attribute_value("role"),
2198 InterpretedValue::Value("one two")
2199 );
2200 assert_eq!(doc.header().roles(), vec!["one", "two"]);
2201 assert_eq!(doc.roles(), vec!["one", "two"]);
2202 }
2203
2204 #[test]
2205 fn roles_empty_without_block_attribute() {
2206 let doc = Parser::default().parse("= Document Title\n\nBody.");
2209
2210 assert!(doc.header().roles().is_empty());
2211 assert!(doc.roles().is_empty());
2212 }
2213
2214 #[test]
2215 fn options_block_attribute_above_title() {
2216 let doc = Parser::default().parse("[opts=\"noheader,autowidth\"]\n= Document Title");
2219
2220 assert!(doc.is_attribute_set("noheader-option"));
2221 assert!(doc.is_attribute_set("autowidth-option"));
2222
2223 let doc = Parser::default().parse("[%hardbreaks]\n= Document Title");
2225 assert!(doc.is_attribute_set("hardbreaks-option"));
2226 }
2227
2228 #[test]
2229 fn bracketed_line_that_is_not_a_separator_attribute_list() {
2230 let doc = Parser::default().parse("[[anchor]]\n= Some Title: Subtitle");
2236 let header = doc.header();
2237
2238 assert_eq!(header.title(), None);
2239 assert_eq!(header.subtitle(), None);
2240
2241 let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
2242 let header = doc.header();
2243
2244 assert_eq!(header.title(), None);
2245 assert_eq!(header.subtitle(), None);
2246 }
2247
2248 #[test]
2249 fn empty_title_separator_falls_back_to_default() {
2250 let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
2253 let header = doc.header();
2254
2255 assert_eq!(header.main_title(), Some("Main Title"));
2256 assert_eq!(header.subtitle(), Some("Subtitle"));
2257 }
2258
2259 #[test]
2260 fn counter_does_not_shadow_title_separator() {
2261 let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
2267 let header = doc.header();
2268
2269 assert_eq!(header.main_title(), Some("Main Title"));
2270 assert_eq!(header.subtitle(), Some("Subtitle 1"));
2271 }
2272
2273 #[test]
2274 fn skips_block_comment_before_author() {
2275 let doc = Parser::default()
2279 .parse("= Title\n////\nAsciidoctor\nrelease artist\n////\nRyan Waldron");
2280 let header = doc.header();
2281
2282 let author = header.authors().first().unwrap();
2283 assert_eq!(author.name(), "Ryan Waldron");
2284
2285 assert_eq!(header.comments().count(), 1);
2286 assert_eq!(
2287 header.comments().next().unwrap().data(),
2288 "////\nAsciidoctor\nrelease artist\n////"
2289 );
2290 }
2291
2292 #[test]
2293 fn skips_block_comment_with_blank_lines() {
2294 let doc = Parser::default().parse("= Title\n////\n\nAsciidoctor\n\n////\nRyan Waldron");
2297 let header = doc.header();
2298
2299 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2300 assert_eq!(header.comments().count(), 1);
2301 }
2302
2303 #[test]
2304 fn unterminated_block_comment_consumes_rest_of_header() {
2305 let doc = Parser::default().parse("= Title\n////\nAsciidoctor\nRyan Waldron");
2308 let header = doc.header();
2309
2310 assert!(header.authors().is_empty());
2311 assert_eq!(header.comments().count(), 1);
2312 }
2313
2314 #[test]
2315 fn longer_block_comment_delimiter_requires_matching_close() {
2316 let doc = Parser::default()
2320 .parse("= Title\n/////\nAsciidoctor\n////\nstill comment\n/////\nRyan Waldron");
2321 let header = doc.header();
2322
2323 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2324 assert_eq!(header.comments().count(), 1);
2325 }
2326
2327 #[test]
2328 fn three_slashes_is_not_a_block_comment() {
2329 let mut parser = Parser::default();
2335 let _ = parser.parse("= Title\nJoe Cool\nv1.0\n///\nstuff");
2336
2337 assert_eq!(
2338 parser.attribute_value("author"),
2339 InterpretedValue::Value("Joe Cool")
2340 );
2341 assert_eq!(
2342 parser.attribute_value("revnumber"),
2343 InterpretedValue::Value("1.0")
2344 );
2345 }
2346
2347 mod markdown_style_document_title {
2348 use crate::tests::prelude::*;
2349
2350 #[test]
2351 fn hash_marker_is_a_document_title() {
2352 let mut parser = Parser::default();
2353 let mi =
2354 crate::document::Header::parse(crate::Span::new("# Just the Title"), &mut parser)
2355 .unwrap_if_no_warnings();
2356
2357 assert_eq!(
2358 mi.item,
2359 Header {
2360 title_source: Some(Span {
2361 data: "Just the Title",
2362 line: 1,
2363 col: 3,
2364 offset: 2,
2365 }),
2366 title: Some("Just the Title"),
2367 attributes: &[],
2368 author_line: None,
2369 revision_line: None,
2370 comments: &[],
2371 source: Span {
2372 data: "# Just the Title",
2373 line: 1,
2374 col: 1,
2375 offset: 0,
2376 }
2377 }
2378 );
2379
2380 assert_eq!(
2381 mi.after,
2382 Span {
2383 data: "",
2384 line: 1,
2385 col: 17,
2386 offset: 16
2387 }
2388 );
2389 }
2390
2391 #[test]
2392 fn sets_doctitle_attribute() {
2393 let doc = Parser::default().parse("# Doc Title\n\n{doctitle}");
2394 assert_eq!(doc.header().title(), Some("Doc Title"));
2395 assert_eq!(rendered_paragraphs(&doc), vec!["Doc Title"]);
2396 }
2397
2398 #[test]
2399 fn strips_symmetric_close() {
2400 let doc = Parser::default().parse("# Doc Title #");
2401 assert_eq!(doc.header().title(), Some("Doc Title"));
2402 }
2403
2404 #[test]
2405 fn does_not_strip_mismatched_close() {
2406 let doc = Parser::default().parse("# Doc Title =");
2409 assert_eq!(doc.header().title(), Some("Doc Title ="));
2410 }
2411
2412 #[test]
2413 fn requires_whitespace_after_marker() {
2414 let doc = Parser::default().parse("#Doc Title");
2415
2416 assert_eq!(doc.header().title(), None);
2417 assert_eq!(rendered_paragraphs(&doc), vec!["#Doc Title"]);
2418 }
2419
2420 #[test]
2421 fn carries_the_rest_of_the_header() {
2422 let doc = Parser::default()
2425 .parse("# Doc Title\n:foo: bar\nKismet R. Lee <kismet@asciidoctor.org>\nv1.0\n");
2426 let header = doc.header();
2427
2428 assert_eq!(header.title(), Some("Doc Title"));
2429 assert_eq!(header.authors().first().unwrap().firstname(), "Kismet");
2430 assert_eq!(header.revision_line().unwrap().revnumber().unwrap(), "1.0");
2431 }
2432
2433 #[test]
2434 fn partitions_subtitle() {
2435 let doc = Parser::default().parse("# Main Title: Subtitle");
2436 let header = doc.header();
2437
2438 assert_eq!(header.main_title(), Some("Main Title"));
2439 assert_eq!(header.subtitle(), Some("Subtitle"));
2440 }
2441
2442 #[test]
2443 fn separator_block_attribute_above_title() {
2444 let doc = Parser::default().parse("[separator=::]\n# Main Title:: Subtitle");
2447 let header = doc.header();
2448
2449 assert_eq!(header.main_title(), Some("Main Title"));
2450 assert_eq!(header.subtitle(), Some("Subtitle"));
2451 }
2452
2453 #[test]
2454 fn markdown_title_followed_by_markdown_sections() {
2455 let doc = Parser::default().parse("# Doc Title\n\n## Section One\n\nblah blah\n");
2456
2457 assert_eq!(doc.header().title(), Some("Doc Title"));
2458
2459 let section = first_section(&doc);
2460
2461 assert_eq!(section.level(), 1);
2462 assert_eq!(section.section_title(), "Section One");
2463 }
2464 }
2465}