1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::{Attrlist, AttrlistContext},
6 content::{Content, SubstitutionGroup},
7 document::{
8 Attribute, Author, AuthorLine, InterpretedValue, RevisionLine, matches_author_pattern,
9 },
10 internal::debug::DebugSliceReference,
11 span::MatchedItem,
12 warnings::{MatchAndWarnings, Warning, WarningType},
13};
14
15#[derive(Clone, Eq, PartialEq)]
19pub struct Header<'src> {
20 title_source: Option<Span<'src>>,
21 title: Option<String>,
22 doctitle: Option<String>,
23 main_title: Option<String>,
24 subtitle: Option<String>,
25 id: Option<String>,
26 roles: Vec<String>,
27 attributes: Vec<Attribute<'src>>,
28 author_line: Option<AuthorLine<'src>>,
29 authors: Vec<Author>,
30 revision_line: Option<RevisionLine<'src>>,
31 comments: Vec<Span<'src>>,
32 source: Span<'src>,
33}
34
35impl<'src> Header<'src> {
36 pub(crate) fn parse(
37 mut source: Span<'src>,
38 parser: &mut Parser,
39 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
40 let original_source = source.discard_empty_lines();
41
42 let mut title_source: Option<Span<'src>> = None;
43 let mut title: Option<String> = None;
44
45 let mut saw_implicit_title = false;
51 let mut implicit_overridden_from_above = false;
52 let mut implicit_doctitle_str: Option<String> = None;
53 let mut doctitle_entry_after_title = false;
54
55 let mut id: Option<String> = None;
56 let mut roles: Vec<String> = vec![];
57 let mut attributes: Vec<Attribute> = vec![];
58 let mut author_line: Option<AuthorLine<'src>> = None;
59 let mut author_attribute: Option<Author> = None;
60 let mut authorinitials_from_entry = false;
61 let mut revision_line: Option<RevisionLine<'src>> = None;
62 let mut comments: Vec<Span<'src>> = vec![];
63 let mut warnings: Vec<Warning<'src>> = vec![];
64
65 while !source.is_empty() {
67 let line_mi = source.take_normalized_line();
68 let line = line_mi.item;
69
70 if line.is_empty() {
72 if title.is_some() {
73 break;
74 }
75 source = line_mi.after;
76 } else if line.starts_with("//") && !line.starts_with("///") {
77 comments.push(line);
78 source = line_mi.after;
79 } else if title.is_some()
80 && let Some((after, terminated)) = skip_block_comment(line, line_mi.after)
81 {
82 comments.push(source.trim_remainder(after).trim_trailing_line_end());
94
95 if !terminated {
101 warnings.push(Warning {
102 source: line,
103 warning: WarningType::UnterminatedDelimitedBlock,
104 origin: None,
105 });
106 }
107
108 source = after;
109 } else if line.starts_with(':')
110 && let Some(attr) = Attribute::parse(source, parser)
111 {
112 if attr
120 .item
121 .name()
122 .data()
123 .eq_ignore_ascii_case("authorinitials")
124 {
125 authorinitials_from_entry =
126 !matches!(attr.item.value(), InterpretedValue::Unset);
127 }
128
129 let mut author_name_override: Option<String> = None;
137 if attr.item.name().data().eq_ignore_ascii_case("author")
138 && let Some(raw_value) = attr.item.raw_value()
139 && let Some(author) = Author::parse(raw_value.data(), parser, true)
140 {
141 parser.set_attribute_by_value_from_header("firstname", author.firstname());
143 if let Some(middlename) = author.middlename() {
144 parser.set_attribute_by_value_from_header("middlename", middlename);
145 }
146 if let Some(lastname) = author.lastname() {
147 parser.set_attribute_by_value_from_header("lastname", lastname);
148 }
149
150 if !authorinitials_from_entry {
153 parser.set_attribute_by_value_from_header(
154 "authorinitials",
155 author.initials(),
156 );
157 }
158
159 if let Some(email) = author.email() {
160 parser.set_attribute_by_value_from_header("email", email);
161 }
162
163 let raw = raw_value.data();
172 if !raw.contains('<') && !raw.contains('{') && !matches_author_pattern(raw) {
173 author_name_override = Some(author.name().to_string());
174 }
175
176 author_attribute = Some(author);
181 }
182
183 parser.set_attribute_from_header(&attr.item, &mut warnings);
184
185 if let Some(author_name) = author_name_override {
186 parser.set_attribute_by_value_from_header("author", author_name);
187 }
188
189 if title.is_some() && attr.item.name().data().eq_ignore_ascii_case("doctitle") {
193 doctitle_entry_after_title = true;
194 }
195
196 attributes.push(attr.item);
197 source = attr.after;
198 } else if title.is_none()
199 && line.starts_with('[')
200 && line.ends_with(']')
201 && document_title_follows_block_metadata(line_mi.after)
202 && let Some((metadata, metadata_warnings)) =
203 parse_document_metadata_attrlist(line, parser)
204 {
205 warnings.extend(metadata_warnings);
206 if let Some(doc_id) = metadata.id {
224 id = Some(doc_id);
225 }
226 if let Some(separator) = metadata.separator {
227 parser.set_attribute_by_value_from_header("title-separator", separator);
228 }
229 if let Some(reftext) = metadata.reftext {
230 parser.set_attribute_by_value_from_header("reftext", reftext);
231 }
232 if !metadata.roles.is_empty() {
233 roles.extend(metadata.roles);
240 parser.set_attribute_by_value_from_header("role", roles.join(" "));
241 }
242 for option in metadata.options {
243 parser.set_attribute_by_value_from_header(format!("{option}-option"), "");
244 }
245 source = line_mi.after;
246 } else if title.is_none()
247 && let Some(marker) = document_title_marker(line)
248 {
249 let title_span = crate::blocks::strip_symmetric_title_close(
252 line.discard(2).discard_whitespace(),
253 marker,
254 1,
255 );
256 saw_implicit_title = true;
257
258 title_source = Some(title_span);
259
260 if let InterpretedValue::Value(existing) = parser.attribute_value("doctitle")
268 && !existing.is_empty()
269 {
270 implicit_overridden_from_above = true;
271 implicit_doctitle_str = Some(existing.clone());
272 title = Some(existing);
273 } else {
274 let title_str = apply_header_subs(title_span.data(), parser);
275
276 parser.set_attribute_by_value_from_header("doctitle", &title_str);
277
278 implicit_doctitle_str = Some(title_str.clone());
279 title = Some(title_str);
280 }
281
282 source = line_mi.after;
283 } else if title.is_some() && author_line.is_none() {
284 author_line = Some(AuthorLine::parse(line, parser));
285 source = line_mi.after;
286 } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
287 revision_line = Some(RevisionLine::parse(line, parser));
288 source = line_mi.after;
289 } else {
290 if title.is_some() {
291 warnings.push(Warning {
292 source: line,
293 warning: WarningType::DocumentHeaderNotTerminated,
294 origin: None,
295 });
296 }
297 break;
298 }
299 }
300
301 let after = source.discard_empty_lines();
302 let source = original_source.trim_remainder(source);
303
304 let final_doctitle_attr = match parser.attribute_value("doctitle") {
314 InterpretedValue::Value(v) if !v.is_empty() => Some(v),
315 _ => None,
316 };
317
318 title = if saw_implicit_title {
319 let base = if !implicit_overridden_from_above
336 && let Some(raw) = title_source
337 && implicit_doctitle_str
338 .as_deref()
339 .is_some_and(|s| s.contains('{'))
340 {
341 Some(apply_header_subs(raw.data(), parser))
342 } else {
343 title
344 };
345
346 if doctitle_entry_after_title
350 && let Some(ref dt) = final_doctitle_attr
351 && Some(dt) != implicit_doctitle_str.as_ref()
352 {
353 Some(dt.clone())
354 } else {
355 base
356 }
357 } else {
358 final_doctitle_attr
361 };
362
363 let (main_title, subtitle) = match &title {
368 Some(title) => {
369 let (main_title, subtitle) = partition_title(title, parser);
370 (Some(main_title), subtitle)
371 }
372 None => (None, None),
373 };
374
375 let doctitle = match parser.attribute_value("title") {
379 InterpretedValue::Value(v) => Some(v),
380 InterpretedValue::Set => Some(String::new()),
381 InterpretedValue::Unset => title.clone(),
382 };
383
384 let authors = resolve_authors(
390 author_line.as_ref(),
391 author_attribute,
392 !attributes.is_empty(),
393 parser,
394 );
395
396 if !authors.is_empty() {
401 parser.set_attribute_by_value_from_header("authorcount", authors.len().to_string());
402 }
403
404 MatchAndWarnings {
405 item: MatchedItem {
406 item: Self {
407 title_source,
408 title,
409 doctitle,
410 main_title,
411 subtitle,
412 id,
413 roles,
414 attributes,
415 author_line,
416 authors,
417 revision_line,
418 comments,
419 source: source.trim_trailing_whitespace(),
420 },
421 after,
422 },
423 warnings,
424 }
425 }
426
427 pub fn title_source(&'src self) -> Option<Span<'src>> {
429 self.title_source
430 }
431
432 pub fn title(&self) -> Option<&str> {
442 self.title.as_deref()
443 }
444
445 pub(crate) fn doctitle(&self) -> Option<&str> {
457 self.doctitle.as_deref()
458 }
459
460 pub fn main_title(&self) -> Option<&str> {
470 self.main_title.as_deref()
471 }
472
473 pub fn subtitle(&self) -> Option<&str> {
480 self.subtitle.as_deref()
481 }
482
483 pub fn id(&self) -> Option<&str> {
489 self.id.as_deref()
490 }
491
492 pub fn roles(&self) -> Vec<&str> {
501 self.roles.iter().map(String::as_str).collect()
502 }
503
504 pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
506 self.attributes.iter()
507 }
508
509 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
511 self.author_line.as_ref()
512 }
513
514 pub fn authors(&self) -> &[Author] {
523 &self.authors
524 }
525
526 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
528 self.revision_line.as_ref()
529 }
530
531 pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
533 self.comments.iter()
534 }
535}
536
537impl<'src> HasSpan<'src> for Header<'src> {
538 fn span(&self) -> Span<'src> {
539 self.source
540 }
541}
542
543fn skip_block_comment<'src>(line: Span<'src>, after: Span<'src>) -> Option<(Span<'src>, bool)> {
558 let delimiter = line.data();
559 if delimiter.len() < 4 || !delimiter.bytes().all(|b| b == b'/') {
560 return None;
561 }
562
563 let mut next = after;
564 let mut terminated = false;
565 while !next.is_empty() {
566 let line_mi = next.take_normalized_line();
567 next = line_mi.after;
568 if line_mi.item.data() == delimiter {
569 terminated = true;
570 break;
571 }
572 }
573
574 Some((next, terminated))
575}
576
577fn document_title_marker(line: Span<'_>) -> Option<char> {
585 if line.starts_with("= ") {
586 Some('=')
587 } else if line.starts_with("# ") {
588 Some('#')
589 } else {
590 None
591 }
592}
593
594fn document_title_follows_block_metadata(after: Span<'_>) -> bool {
610 let mut next = after;
611
612 while !next.is_empty() {
613 let line_mi = next.take_normalized_line();
614 let line = line_mi.item;
615
616 if document_title_marker(line).is_some() {
617 return true;
618 }
619
620 if !is_document_metadata_line(line) {
621 return false;
622 }
623
624 next = line_mi.after;
625 }
626
627 false
628}
629
630fn is_document_metadata_line(line: Span<'_>) -> bool {
642 if !(line.starts_with('[') && line.ends_with(']')) {
643 return false;
644 }
645
646 let inner = line.slice(1..line.len() - 1);
647
648 !(inner.is_empty()
649 || inner.starts_with(' ')
650 || inner.starts_with('\t')
651 || (inner.starts_with('[') && inner.ends_with(']')))
652}
653
654struct DocumentMetadata {
660 id: Option<String>,
661 separator: Option<String>,
662 reftext: Option<String>,
663 roles: Vec<String>,
664 options: Vec<String>,
665}
666
667fn parse_document_metadata_attrlist<'src>(
679 line: Span<'src>,
680 parser: &Parser,
681) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
682 if !is_document_metadata_line(line) {
687 return None;
688 }
689
690 let inner = line.slice(1..line.len() - 1);
692
693 let MatchAndWarnings {
694 item: MatchedItem {
695 item: attrlist,
696 after: _,
697 },
698 warnings,
699 } = Attrlist::parse(inner, parser, AttrlistContext::Block);
700
701 let metadata = DocumentMetadata {
702 id: attrlist.id().map(str::to_string),
703 separator: attrlist
704 .named_attribute("separator")
705 .map(|attr| attr.value().to_string()),
706 reftext: attrlist
707 .named_attribute("reftext")
708 .map(|attr| attr.value().to_string()),
709 roles: attrlist.roles().iter().map(|r| r.to_string()).collect(),
710 options: attrlist.options().iter().map(|o| o.to_string()).collect(),
711 };
712
713 Some((metadata, warnings))
714}
715
716fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
724 let separator = match parser.effective_attribute("title-separator") {
729 Some(av) => match &av.value {
730 InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
731 _ => ":".to_string(),
732 },
733 None => ":".to_string(),
734 };
735
736 let separator = format!("{separator} ");
737
738 match title.rfind(&separator) {
739 Some(index) => {
740 let main_title = title[..index].to_string();
741 let subtitle = title[index + separator.len()..].to_string();
742 (main_title, Some(subtitle))
743 }
744 None => (title.to_string(), None),
745 }
746}
747
748fn resolve_authors(
775 author_line: Option<&AuthorLine>,
776 author_attribute: Option<Author>,
777 header_has_attributes: bool,
778 parser: &mut Parser,
779) -> Vec<Author> {
780 if let Some(author_line) = author_line {
781 return author_line.authors().cloned().collect();
782 }
783
784 if !header_has_attributes {
785 return vec![];
786 }
787
788 if attribute_string(parser, "author").is_some()
795 && let Some(author) = author_attribute
796 {
797 return vec![author.with_email(attribute_string(parser, "email"))];
798 }
799
800 if let Some(authors_value) = attribute_string(parser, "authors") {
803 let authors = collect_indexed_authors(
804 split_author_entries(&authors_value)
805 .into_iter()
806 .filter_map(|entry| Author::parse(entry, parser, true)),
807 parser,
808 );
809
810 if !authors.is_empty() {
811 set_author_metadata(parser, &authors);
812 return authors;
813 }
814 }
815
816 let mut raw_names = vec![];
818 let mut index = 1;
819
820 while let Some(name) = attribute_string(parser, &format!("author_{index}")) {
821 raw_names.push(name);
822 index += 1;
823 }
824
825 let authors = collect_indexed_authors(
826 raw_names
827 .iter()
828 .filter_map(|name| Author::parse(name, parser, true)),
829 parser,
830 );
831
832 if !authors.is_empty() {
833 set_author_metadata(parser, &authors);
834 }
835
836 authors
837}
838
839fn attribute_string(parser: &Parser, name: &str) -> Option<String> {
842 match parser.attribute_value(name) {
843 InterpretedValue::Value(value) => Some(value),
844 _ => None,
845 }
846}
847
848fn collect_indexed_authors(authors: impl Iterator<Item = Author>, parser: &Parser) -> Vec<Author> {
852 authors
853 .enumerate()
854 .map(|(idx, author)| {
855 author.with_email(attribute_string(parser, &format!("email_{}", idx + 1)))
856 })
857 .collect()
858}
859
860fn split_author_entries(value: &str) -> Vec<&str> {
867 let bytes = value.as_bytes();
868 let mut entries: Vec<&str> = Vec::new();
869 let mut start = 0;
870
871 for (index, c) in value.char_indices() {
872 if c != ';' {
873 continue;
874 }
875
876 let is_separator = match bytes.get(index + 1) {
877 Some(next) => *next == b' ',
878 None => true,
879 };
880
881 if is_separator {
882 entries.push(&value[start..index]);
883 start = index + 1;
884 }
885 }
886
887 entries.push(&value[start..]);
888 entries
889}
890
891fn set_author_metadata(parser: &mut Parser, authors: &[Author]) {
908 for (idx, author) in authors.iter().enumerate() {
909 set_author_keys(parser, author, if idx == 0 { None } else { Some(idx + 1) });
910
911 if idx == 1
913 && let Some(first) = authors.first()
914 {
915 set_author_keys(parser, first, Some(1));
916 }
917 }
918
919 let joined = authors
920 .iter()
921 .map(Author::name)
922 .collect::<Vec<_>>()
923 .join(", ");
924
925 parser.set_attribute_by_value_from_header("authors", joined);
926}
927
928fn set_author_keys(parser: &mut Parser, author: &Author, index: Option<usize>) {
931 let key = |name: &str| match index {
932 None => name.to_string(),
933 Some(n) => format!("{name}_{n}"),
934 };
935
936 parser.set_attribute_by_value_from_header(key("author"), author.name());
937 parser.set_attribute_by_value_from_header(key("firstname"), author.firstname());
938
939 if let Some(middlename) = author.middlename() {
940 parser.set_attribute_by_value_from_header(key("middlename"), middlename);
941 }
942
943 if let Some(lastname) = author.lastname() {
944 parser.set_attribute_by_value_from_header(key("lastname"), lastname);
945 }
946
947 parser.set_attribute_by_value_from_header(key("authorinitials"), author.initials());
948
949 if let Some(email) = author.email() {
950 parser.set_attribute_by_value_from_header(key("email"), email);
951 }
952}
953
954fn apply_header_subs(source: &str, parser: &Parser) -> String {
955 let span = Span::new(source);
956
957 let mut content = Content::from(span);
958 SubstitutionGroup::Header.apply(&mut content, parser, None);
959
960 content.rendered().to_string()
961}
962
963impl std::fmt::Debug for Header<'_> {
964 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965 f.debug_struct("Header")
966 .field("title_source", &self.title_source)
967 .field("title", &self.title)
968 .field("doctitle", &self.doctitle)
969 .field("main_title", &self.main_title)
970 .field("subtitle", &self.subtitle)
971 .field("id", &self.id)
972 .field("roles", &self.roles)
973 .field("attributes", &DebugSliceReference(&self.attributes))
974 .field("author_line", &self.author_line)
975 .field("authors", &self.authors)
976 .field("revision_line", &self.revision_line)
977 .field("comments", &DebugSliceReference(&self.comments))
978 .field("source", &self.source)
979 .finish()
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 #![allow(clippy::unwrap_used)]
986
987 use crate::tests::prelude::*;
988
989 #[test]
990 fn impl_clone() {
991 let mut parser = Parser::default();
993
994 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
995 .unwrap_if_no_warnings();
996 let h2 = h1.clone();
997
998 assert_eq!(h1, h2);
999 }
1000
1001 #[test]
1002 fn only_title() {
1003 let mut parser = Parser::default();
1004 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1005 .unwrap_if_no_warnings();
1006
1007 assert_eq!(
1008 mi.item,
1009 Header {
1010 title_source: Some(Span {
1011 data: "Just the Title",
1012 line: 1,
1013 col: 3,
1014 offset: 2,
1015 }),
1016 title: Some("Just the Title"),
1017 attributes: &[],
1018 author_line: None,
1019 revision_line: None,
1020 comments: &[],
1021 source: Span {
1022 data: "= Just the Title",
1023 line: 1,
1024 col: 1,
1025 offset: 0,
1026 }
1027 }
1028 );
1029
1030 assert_eq!(
1031 mi.after,
1032 Span {
1033 data: "",
1034 line: 1,
1035 col: 17,
1036 offset: 16
1037 }
1038 );
1039 }
1040
1041 #[test]
1042 fn trims_leading_spaces_in_title() {
1043 let mut parser = Parser::default();
1046 let mi =
1047 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1048 .unwrap_if_no_warnings();
1049
1050 assert_eq!(
1051 mi.item,
1052 Header {
1053 title_source: Some(Span {
1054 data: "Just the Title",
1055 line: 1,
1056 col: 6,
1057 offset: 5,
1058 }),
1059 title: Some("Just the Title"),
1060 attributes: &[],
1061 author_line: None,
1062 revision_line: None,
1063 comments: &[],
1064 source: Span {
1065 data: "= Just the Title",
1066 line: 1,
1067 col: 1,
1068 offset: 0,
1069 }
1070 }
1071 );
1072
1073 assert_eq!(
1074 mi.after,
1075 Span {
1076 data: "",
1077 line: 1,
1078 col: 20,
1079 offset: 19
1080 }
1081 );
1082 }
1083
1084 #[test]
1085 fn trims_trailing_spaces_in_title() {
1086 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: 3,
1098 offset: 2,
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 title_and_attribute() {
1127 let mut parser = Parser::default();
1128
1129 let mi = crate::document::Header::parse(
1130 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
1131 &mut parser,
1132 )
1133 .unwrap_if_no_warnings();
1134
1135 assert_eq!(
1136 mi.item,
1137 Header {
1138 title_source: Some(Span {
1139 data: "Just the Title",
1140 line: 1,
1141 col: 3,
1142 offset: 2,
1143 }),
1144 title: Some("Just the Title"),
1145 attributes: &[Attribute {
1146 name: Span {
1147 data: "foo",
1148 line: 2,
1149 col: 2,
1150 offset: 18,
1151 },
1152 value_source: Some(Span {
1153 data: "bar",
1154 line: 2,
1155 col: 7,
1156 offset: 23,
1157 }),
1158 value: InterpretedValue::Value("bar"),
1159 source: Span {
1160 data: ":foo: bar",
1161 line: 2,
1162 col: 1,
1163 offset: 17,
1164 }
1165 }],
1166 author_line: None,
1167 revision_line: None,
1168 comments: &[],
1169 source: Span {
1170 data: "= Just the Title\n:foo: bar",
1171 line: 1,
1172 col: 1,
1173 offset: 0,
1174 }
1175 }
1176 );
1177
1178 assert_eq!(
1179 mi.after,
1180 Span {
1181 data: "blah",
1182 line: 4,
1183 col: 1,
1184 offset: 28
1185 }
1186 );
1187 }
1188
1189 #[test]
1190 fn title_applies_header_substitutions() {
1191 let mut parser = Parser::default();
1192
1193 let mi = crate::document::Header::parse(
1194 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
1195 &mut parser,
1196 )
1197 .unwrap_if_no_warnings();
1198
1199 assert_eq!(
1200 mi.item,
1201 Header {
1202 title_source: Some(Span {
1203 data: "The Title & Some{sp}Nonsense",
1204 line: 1,
1205 col: 3,
1206 offset: 2,
1207 }),
1208 title: Some("The Title & Some Nonsense"),
1209 attributes: &[Attribute {
1210 name: Span {
1211 data: "foo",
1212 line: 2,
1213 col: 2,
1214 offset: 32,
1215 },
1216 value_source: Some(Span {
1217 data: "bar",
1218 line: 2,
1219 col: 7,
1220 offset: 37,
1221 }),
1222 value: InterpretedValue::Value("bar"),
1223 source: Span {
1224 data: ":foo: bar",
1225 line: 2,
1226 col: 1,
1227 offset: 31,
1228 }
1229 }],
1230 author_line: None,
1231 revision_line: None,
1232 comments: &[],
1233 source: Span {
1234 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
1235 line: 1,
1236 col: 1,
1237 offset: 0,
1238 }
1239 }
1240 );
1241
1242 assert_eq!(
1243 mi.after,
1244 Span {
1245 data: "blah",
1246 line: 4,
1247 col: 1,
1248 offset: 42
1249 }
1250 );
1251 }
1252
1253 #[test]
1254 fn attribute_without_title() {
1255 let mut parser = Parser::default();
1256 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
1257 .unwrap_if_no_warnings();
1258
1259 assert_eq!(
1260 mi.item,
1261 Header {
1262 title_source: None,
1263 title: None,
1264 attributes: &[Attribute {
1265 name: Span {
1266 data: "foo",
1267 line: 1,
1268 col: 2,
1269 offset: 1,
1270 },
1271 value_source: Some(Span {
1272 data: "bar",
1273 line: 1,
1274 col: 7,
1275 offset: 6,
1276 }),
1277 value: InterpretedValue::Value("bar"),
1278 source: Span {
1279 data: ":foo: bar",
1280 line: 1,
1281 col: 1,
1282 offset: 0,
1283 }
1284 }],
1285 author_line: None,
1286 revision_line: None,
1287 comments: &[],
1288 source: Span {
1289 data: ":foo: bar",
1290 line: 1,
1291 col: 1,
1292 offset: 0,
1293 }
1294 }
1295 );
1296
1297 assert_eq!(
1298 mi.after,
1299 Span {
1300 data: "blah",
1301 line: 3,
1302 col: 1,
1303 offset: 11
1304 }
1305 );
1306 }
1307
1308 #[test]
1309 fn sets_doctitle_attribute() {
1310 let mut parser = Parser::default();
1311 let _doc = parser.parse("= Document Title Goes Here");
1312
1313 assert_eq!(
1314 parser.attribute_value("doctitle"),
1315 InterpretedValue::Value("Document Title Goes Here")
1316 );
1317 }
1318
1319 #[test]
1320 fn sets_author_attributes_from_author_attribute() {
1321 let mut parser = Parser::default();
1322 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
1323
1324 assert_eq!(
1326 parser.attribute_value("firstname"),
1327 InterpretedValue::Value("John")
1328 );
1329 assert_eq!(
1330 parser.attribute_value("middlename"),
1331 InterpretedValue::Value("Q.")
1332 );
1333 assert_eq!(
1334 parser.attribute_value("lastname"),
1335 InterpretedValue::Value("Smith")
1336 );
1337 assert_eq!(
1338 parser.attribute_value("authorinitials"),
1339 InterpretedValue::Value("JQS")
1340 );
1341 assert_eq!(
1342 parser.attribute_value("email"),
1343 InterpretedValue::Value("john@example.com")
1344 );
1345
1346 assert_eq!(
1348 parser.attribute_value("author"),
1349 InterpretedValue::Value("John Q. Smith <john@example.com>")
1350 );
1351 }
1352
1353 #[test]
1354 fn author_attribute_with_four_or_more_parts_is_partitioned() {
1355 let mut parser = Parser::default();
1361 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr.");
1362
1363 assert_eq!(
1364 parser.attribute_value("author"),
1365 InterpretedValue::Value("Leroy Harold Scherer, Jr.")
1366 );
1367 assert_eq!(
1368 parser.attribute_value("firstname"),
1369 InterpretedValue::Value("Leroy")
1370 );
1371 assert_eq!(
1372 parser.attribute_value("middlename"),
1373 InterpretedValue::Value("Harold")
1374 );
1375 assert_eq!(
1376 parser.attribute_value("lastname"),
1377 InterpretedValue::Value("Scherer, Jr.")
1378 );
1379 assert_eq!(
1380 parser.attribute_value("authorinitials"),
1381 InterpretedValue::Value("LHS")
1382 );
1383 }
1384
1385 #[test]
1386 fn author_attribute_two_part_fallback_partitions_lastname() {
1387 let mut parser = Parser::default();
1391 let _doc = parser.parse(":author: Jane, Doe");
1392
1393 assert_eq!(
1394 parser.attribute_value("author"),
1395 InterpretedValue::Value("Jane, Doe")
1396 );
1397 assert_eq!(
1398 parser.attribute_value("firstname"),
1399 InterpretedValue::Value("Jane,")
1400 );
1401 assert_eq!(
1402 parser.attribute_value("middlename"),
1403 InterpretedValue::Unset
1404 );
1405 assert_eq!(
1406 parser.attribute_value("lastname"),
1407 InterpretedValue::Value("Doe")
1408 );
1409 }
1410
1411 #[test]
1412 fn author_attribute_single_part_fallback_is_firstname_only() {
1413 let mut parser = Parser::default();
1416 let _doc = parser.parse(":author: Jane,");
1417
1418 assert_eq!(
1419 parser.attribute_value("author"),
1420 InterpretedValue::Value("Jane,")
1421 );
1422 assert_eq!(
1423 parser.attribute_value("firstname"),
1424 InterpretedValue::Value("Jane,")
1425 );
1426 assert_eq!(
1427 parser.attribute_value("middlename"),
1428 InterpretedValue::Unset
1429 );
1430 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1431 }
1432
1433 #[test]
1434 fn author_attribute_four_or_more_parts_with_inline_email() {
1435 let mut parser = Parser::default();
1439 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr. <leroy@example.com>");
1440
1441 assert_eq!(
1442 parser.attribute_value("firstname"),
1443 InterpretedValue::Value("Leroy")
1444 );
1445 assert_eq!(
1446 parser.attribute_value("middlename"),
1447 InterpretedValue::Value("Harold")
1448 );
1449 assert_eq!(
1450 parser.attribute_value("lastname"),
1451 InterpretedValue::Value("Scherer, Jr.")
1452 );
1453 assert_eq!(
1454 parser.attribute_value("email"),
1455 InterpretedValue::Value("leroy@example.com")
1456 );
1457 assert_eq!(
1458 parser.attribute_value("authorinitials"),
1459 InterpretedValue::Value("LHS")
1460 );
1461 }
1462
1463 #[test]
1464 fn author_attribute_reference_expands_and_partitions() {
1465 let mut parser = Parser::default();
1469 let _doc = parser.parse(":full-name: Leroy Harold Scherer, Jr.\n:author: {full-name}");
1470
1471 assert_eq!(
1472 parser.attribute_value("firstname"),
1473 InterpretedValue::Value("Leroy")
1474 );
1475 assert_eq!(
1476 parser.attribute_value("middlename"),
1477 InterpretedValue::Value("Harold")
1478 );
1479 assert_eq!(
1480 parser.attribute_value("lastname"),
1481 InterpretedValue::Value("Scherer, Jr.")
1482 );
1483 assert_eq!(
1484 parser.attribute_value("authorinitials"),
1485 InterpretedValue::Value("LHS")
1486 );
1487 }
1488
1489 #[test]
1490 fn author_attribute_reference_within_larger_value_expands_and_partitions() {
1491 let mut parser = Parser::default();
1495 let _doc = parser.parse(":rest: Harold Scherer, Jr.\n:author: Leroy {rest}");
1496
1497 assert_eq!(
1498 parser.attribute_value("firstname"),
1499 InterpretedValue::Value("Leroy")
1500 );
1501 assert_eq!(
1502 parser.attribute_value("middlename"),
1503 InterpretedValue::Value("Harold")
1504 );
1505 assert_eq!(
1506 parser.attribute_value("lastname"),
1507 InterpretedValue::Value("Scherer, Jr.")
1508 );
1509 }
1510
1511 #[test]
1512 fn author_attribute_non_breaking_space_is_not_a_name_separator() {
1513 let mut parser = Parser::default();
1517 let _doc = parser.parse(":author: John\u{a0}Doe Scherer, Jr.");
1518
1519 assert_eq!(
1520 parser.attribute_value("firstname"),
1521 InterpretedValue::Value("John\u{a0}Doe")
1522 );
1523 assert_eq!(
1524 parser.attribute_value("middlename"),
1525 InterpretedValue::Value("Scherer,")
1526 );
1527 assert_eq!(
1528 parser.attribute_value("lastname"),
1529 InterpretedValue::Value("Jr.")
1530 );
1531 }
1532
1533 #[test]
1534 fn sets_author_attributes_from_author_attribute_two_names() {
1535 let mut parser = Parser::default();
1536 let _doc = parser.parse(":author: Jane Doe");
1537
1538 assert_eq!(
1540 parser.attribute_value("firstname"),
1541 InterpretedValue::Value("Jane")
1542 );
1543 assert_eq!(
1544 parser.attribute_value("middlename"),
1545 InterpretedValue::Unset
1546 );
1547 assert_eq!(
1548 parser.attribute_value("lastname"),
1549 InterpretedValue::Value("Doe")
1550 );
1551 assert_eq!(
1552 parser.attribute_value("authorinitials"),
1553 InterpretedValue::Value("JD")
1554 );
1555 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1556 }
1557
1558 #[test]
1559 fn sets_author_attributes_from_author_attribute_single_name() {
1560 let mut parser = Parser::default();
1561 let _doc = parser.parse(":author: Cher");
1562
1563 assert_eq!(
1565 parser.attribute_value("firstname"),
1566 InterpretedValue::Value("Cher")
1567 );
1568 assert_eq!(
1569 parser.attribute_value("middlename"),
1570 InterpretedValue::Unset
1571 );
1572 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1573 assert_eq!(
1574 parser.attribute_value("authorinitials"),
1575 InterpretedValue::Value("C")
1576 );
1577 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1578 }
1579
1580 #[test]
1581 fn sets_author_attributes_from_empty_string() {
1582 let mut parser = Parser::default();
1583 let _doc = parser.parse(":author:");
1584
1585 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
1587 assert_eq!(
1588 parser.attribute_value("middlename"),
1589 InterpretedValue::Unset
1590 );
1591 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1592 assert_eq!(
1593 parser.attribute_value("authorinitials"),
1594 InterpretedValue::Unset
1595 );
1596 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1597
1598 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
1599 }
1600
1601 #[test]
1602 fn authors_from_author_line() {
1603 let doc = Parser::default().parse("= Title\nKismet R. Lee <kismet@asciidoctor.org>");
1604
1605 assert_eq!(doc.authors().len(), 1);
1606
1607 let author = doc.authors().first().unwrap();
1608 assert_eq!(author.name(), "Kismet R. Lee");
1609 assert_eq!(author.email(), Some("kismet@asciidoctor.org"));
1610 assert_eq!(author.initials(), "KRL");
1611 }
1612
1613 #[test]
1614 fn authors_from_author_attribute() {
1615 let doc =
1618 Parser::default().parse("= Title\n:author: Jane Q. Public\n:email: jane@example.com");
1619
1620 assert_eq!(doc.authors().len(), 1);
1621
1622 let author = doc.authors().first().unwrap();
1623 assert_eq!(author.name(), "Jane Q. Public");
1624 assert_eq!(author.firstname(), "Jane");
1625 assert_eq!(author.middlename(), Some("Q."));
1626 assert_eq!(author.lastname(), Some("Public"));
1627 assert_eq!(author.email(), Some("jane@example.com"));
1628 assert_eq!(author.initials(), "JQP");
1629 }
1630
1631 #[test]
1632 fn authors_from_author_attribute_with_inline_email() {
1633 let doc = Parser::default().parse("= Title\n:author: John Q. Smith <john@example.com>");
1636
1637 assert_eq!(doc.authors().len(), 1);
1638
1639 let author = doc.authors().first().unwrap();
1640 assert_eq!(author.name(), "John Q. Smith");
1641 assert_eq!(author.firstname(), "John");
1642 assert_eq!(author.middlename(), Some("Q."));
1643 assert_eq!(author.lastname(), Some("Smith"));
1644 assert_eq!(author.email(), Some("john@example.com"));
1645 assert_eq!(author.initials(), "JQS");
1646 }
1647
1648 #[test]
1649 fn authors_is_empty_without_author_info() {
1650 let doc = Parser::default().parse("= Title\n\nBody.");
1651
1652 assert!(doc.authors().is_empty());
1653 }
1654
1655 #[test]
1656 fn authorcount_reflects_author_line() {
1657 let doc = Parser::default().parse("= Title\nJane Doe; John Smith\n\nBody.");
1660
1661 assert_eq!(doc.authors().len(), 2);
1662 assert_eq!(
1663 doc.attribute_value("authorcount"),
1664 InterpretedValue::Value("2")
1665 );
1666
1667 let doc = Parser::default().parse(":author: Jane Doe\n\nBody.");
1669
1670 assert_eq!(
1671 doc.attribute_value("authorcount"),
1672 InterpretedValue::Value("1")
1673 );
1674
1675 let doc = Parser::default().parse("= Title\n\nBody.");
1677
1678 assert_eq!(
1679 doc.attribute_value("authorcount"),
1680 InterpretedValue::Value("0")
1681 );
1682 }
1683
1684 #[test]
1685 fn explicit_authorinitials_after_author_still_wins() {
1686 let doc = Parser::default().parse(":author: Doc Writer\n:authorinitials: DOC\n\nBody.");
1689
1690 assert_eq!(
1691 doc.attribute_value("authorinitials"),
1692 InterpretedValue::Value("DOC")
1693 );
1694
1695 let doc = Parser::default()
1698 .parse(":author: Jane Roe\n:authorinitials: DOC\n:author: Doc Writer\n\nBody.");
1699
1700 assert_eq!(
1701 doc.attribute_value("author"),
1702 InterpretedValue::Value("Doc Writer")
1703 );
1704 assert_eq!(
1705 doc.attribute_value("authorinitials"),
1706 InterpretedValue::Value("DOC")
1707 );
1708 }
1709
1710 #[test]
1711 fn later_author_entry_redrives_initials_without_explicit_override() {
1712 let doc = Parser::default().parse(":author: Jane Roe\n:author: Doc Writer\n\nBody.");
1715
1716 assert_eq!(
1717 doc.attribute_value("authorinitials"),
1718 InterpretedValue::Value("DW")
1719 );
1720 }
1721
1722 #[test]
1723 fn explicit_authorinitials_not_preserved_for_indexed_or_authors_forms() {
1724 let doc = Parser::default().parse(":authorinitials: DOC\n:author_1: Doc Writer\n\nBody.");
1729
1730 assert_eq!(
1731 doc.attribute_value("author"),
1732 InterpretedValue::Value("Doc Writer")
1733 );
1734 assert_eq!(
1735 doc.attribute_value("authorinitials"),
1736 InterpretedValue::Value("DW")
1737 );
1738 }
1739
1740 #[test]
1741 fn authors_attribute_splits_into_indexed_authors() {
1742 let doc = Parser::default().parse(":authors: Jane Doe; John Q. Smith\n\nBody.");
1745
1746 assert_eq!(doc.authors().len(), 2);
1747 assert_eq!(
1748 doc.attribute_value("authors"),
1749 InterpretedValue::Value("Jane Doe, John Q. Smith")
1750 );
1751 assert_eq!(
1752 doc.attribute_value("author"),
1753 InterpretedValue::Value("Jane Doe")
1754 );
1755 assert_eq!(
1756 doc.attribute_value("author_2"),
1757 InterpretedValue::Value("John Q. Smith")
1758 );
1759 assert_eq!(
1760 doc.attribute_value("middlename_2"),
1761 InterpretedValue::Value("Q.")
1762 );
1763 assert_eq!(
1764 doc.attribute_value("authorinitials_2"),
1765 InterpretedValue::Value("JQS")
1766 );
1767 }
1768
1769 #[test]
1770 fn authors_attribute_attaches_companion_emails_and_base_middlename() {
1771 let doc = Parser::default().parse(
1775 ":authors: Jane Q. Doe; John Smith\n:email_1: jane@example.com\n:email_2: john@example.com\n\nBody.",
1776 );
1777
1778 let authors = doc.authors();
1779 assert_eq!(authors.len(), 2);
1780 assert_eq!(authors.first().unwrap().email(), Some("jane@example.com"));
1781 assert_eq!(authors.get(1).unwrap().email(), Some("john@example.com"));
1782
1783 assert_eq!(
1784 doc.attribute_value("middlename"),
1785 InterpretedValue::Value("Q.")
1786 );
1787 assert_eq!(
1788 doc.attribute_value("email"),
1789 InterpretedValue::Value("jane@example.com")
1790 );
1791 assert_eq!(
1792 doc.attribute_value("email_2"),
1793 InterpretedValue::Value("john@example.com")
1794 );
1795 }
1796
1797 #[test]
1798 fn authors_attribute_semicolon_without_space_is_one_author() {
1799 let doc = Parser::default().parse(":authors: Joe Doe;Smith Johnson\n\nBody.");
1802
1803 assert_eq!(doc.authors().len(), 1);
1804 assert_eq!(
1805 doc.attribute_value("authorcount"),
1806 InterpretedValue::Value("1")
1807 );
1808 }
1809
1810 #[test]
1811 fn authors_attribute_single_name_authors_and_trailing_separator() {
1812 let doc = Parser::default().parse(":authors: Cher; Madonna;\n\nBody.");
1815
1816 assert_eq!(doc.authors().len(), 2);
1817 assert_eq!(
1818 doc.attribute_value("authors"),
1819 InterpretedValue::Value("Cher, Madonna")
1820 );
1821 assert_eq!(
1822 doc.attribute_value("author"),
1823 InterpretedValue::Value("Cher")
1824 );
1825 assert_eq!(doc.attribute_value("lastname"), InterpretedValue::Unset);
1826 assert_eq!(
1827 doc.attribute_value("authorinitials"),
1828 InterpretedValue::Value("C")
1829 );
1830 assert_eq!(
1831 doc.attribute_value("author_2"),
1832 InterpretedValue::Value("Madonna")
1833 );
1834 assert_eq!(doc.attribute_value("lastname_2"), InterpretedValue::Unset);
1835 assert_eq!(
1836 doc.attribute_value("authorcount"),
1837 InterpretedValue::Value("2")
1838 );
1839 }
1840
1841 #[test]
1842 fn authors_attribute_with_only_empty_entries_yields_no_authors() {
1843 let doc = Parser::default().parse(":authors: ;\n\nBody.");
1846
1847 assert!(doc.authors().is_empty());
1848 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
1849 assert_eq!(
1850 doc.attribute_value("authorcount"),
1851 InterpretedValue::Value("0")
1852 );
1853
1854 assert_eq!(doc.attribute_value("authors"), InterpretedValue::Value(";"));
1858 }
1859
1860 #[test]
1861 fn author_attribute_takes_precedence_over_authors() {
1862 let doc = Parser::default()
1866 .parse(":author: Solo Writer\n:authors: Jane Doe; John Smith\n\nBody.");
1867
1868 assert_eq!(doc.authors().len(), 1);
1869 assert_eq!(
1870 doc.attribute_value("author"),
1871 InterpretedValue::Value("Solo Writer")
1872 );
1873 assert_eq!(doc.attribute_value("author_2"), InterpretedValue::Unset);
1874 }
1875
1876 #[test]
1877 fn author_unset_after_being_assigned_yields_no_authors() {
1878 let doc = Parser::default().parse("= Title\n:author: Jane Doe\n:author!:\n\nBody.");
1881
1882 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
1883 assert!(doc.authors().is_empty());
1884 }
1885
1886 #[test]
1887 fn impl_debug() {
1888 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
1889 let header = doc.header();
1890
1891 assert_eq!(
1892 format!("{header:#?}"),
1893 r#"Header {
1894 title_source: Some(
1895 Span {
1896 data: "Example Title",
1897 line: 1,
1898 col: 3,
1899 offset: 2,
1900 },
1901 ),
1902 title: Some(
1903 "Example Title",
1904 ),
1905 doctitle: Some(
1906 "Example Title",
1907 ),
1908 main_title: Some(
1909 "Example Title",
1910 ),
1911 subtitle: None,
1912 id: None,
1913 roles: [],
1914 attributes: &[],
1915 author_line: None,
1916 authors: [],
1917 revision_line: None,
1918 comments: &[],
1919 source: Span {
1920 data: "= Example Title",
1921 line: 1,
1922 col: 1,
1923 offset: 0,
1924 },
1925}"#
1926 );
1927 }
1928
1929 #[test]
1930 fn no_subtitle() {
1931 let doc = Parser::default().parse("= Just the Title");
1934 let header = doc.header();
1935
1936 assert_eq!(header.title(), Some("Just the Title"));
1937 assert_eq!(header.main_title(), Some("Just the Title"));
1938 assert_eq!(header.subtitle(), None);
1939 }
1940
1941 #[test]
1942 fn no_title() {
1943 let doc = Parser::default().parse(":foo: bar\n\nbody");
1945 let header = doc.header();
1946
1947 assert_eq!(header.title(), None);
1948 assert_eq!(header.main_title(), None);
1949 assert_eq!(header.subtitle(), None);
1950 }
1951
1952 #[test]
1953 fn colon_without_space_is_not_a_separator() {
1954 let doc = Parser::default().parse("= Ratio 3:1 Explained");
1957 let header = doc.header();
1958
1959 assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
1960 assert_eq!(header.subtitle(), None);
1961 }
1962
1963 #[test]
1964 fn subtitle_available_on_document() {
1965 let doc = Parser::default().parse("= Main Title: Subtitle");
1968
1969 assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
1970 assert_eq!(doc.subtitle(), Some("Subtitle"));
1971 }
1972
1973 #[test]
1974 fn separator_block_attribute_above_title() {
1975 let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
1978 let header = doc.header();
1979
1980 assert_eq!(header.main_title(), Some("Main Title"));
1981 assert_eq!(header.subtitle(), Some("Subtitle"));
1982
1983 let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
1986 let header = doc.header();
1987
1988 assert_eq!(header.main_title(), Some("Main: Title"));
1989 assert_eq!(header.subtitle(), Some("Subtitle"));
1990 }
1991
1992 #[test]
1993 fn separator_attribute_entry_overrides_block_attribute() {
1994 let doc = Parser::default()
1997 .parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
1998 let header = doc.header();
1999
2000 assert_eq!(header.main_title(), Some("Main Title"));
2001 assert_eq!(header.subtitle(), Some("Subtitle"));
2002 }
2003
2004 #[test]
2005 fn unrecognized_block_attribute_above_title_is_consumed() {
2006 let doc = Parser::default().parse("[foo=bar]\n= A Header Title");
2012 let header = doc.header();
2013
2014 assert_eq!(header.title(), Some("A Header Title"));
2015 assert_eq!(header.subtitle(), None);
2016 assert_eq!(doc.attribute_value("foo"), InterpretedValue::Unset);
2017 }
2018
2019 #[test]
2020 fn reftext_block_attribute_above_title() {
2021 let doc =
2025 Parser::default().parse("[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2026 let header = doc.header();
2027
2028 assert_eq!(header.title(), Some("Links & Stuff"));
2029 assert_eq!(
2030 doc.attribute_value("reftext"),
2031 InterpretedValue::Value("Links and Stuff")
2032 );
2033 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2034 }
2035
2036 #[test]
2037 fn id_block_attribute_above_title() {
2038 let doc = Parser::default().parse("[#docid]\n= Document Title\n\nBody.");
2041 let header = doc.header();
2042
2043 assert_eq!(header.title(), Some("Document Title"));
2044 assert_eq!(header.id(), Some("docid"));
2045 assert_eq!(doc.id(), Some("docid"));
2046
2047 let doc = Parser::default().parse("[id=docid]\n= Document Title");
2049 assert_eq!(doc.header().id(), Some("docid"));
2050 }
2051
2052 #[test]
2053 fn stacked_block_attributes_above_title() {
2054 let doc = Parser::default()
2059 .parse("[#docid]\n[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2060 let header = doc.header();
2061
2062 assert_eq!(header.title(), Some("Links & Stuff"));
2063 assert_eq!(header.id(), Some("docid"));
2064 assert_eq!(doc.id(), Some("docid"));
2065 assert_eq!(
2066 doc.attribute_value("reftext"),
2067 InterpretedValue::Value("Links and Stuff")
2068 );
2069 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2070 }
2071
2072 #[test]
2073 fn stacked_block_attributes_combine_roles() {
2074 let doc = Parser::default().parse("[#docid]\n[.one]\n[.two]\n= Document Title");
2078 let header = doc.header();
2079
2080 assert_eq!(header.title(), Some("Document Title"));
2081 assert_eq!(header.id(), Some("docid"));
2082 assert_eq!(
2083 doc.attribute_value("role"),
2084 InterpretedValue::Value("one two")
2085 );
2086 assert_eq!(header.roles(), vec!["one", "two"]);
2087 }
2088
2089 #[test]
2090 fn stacked_block_attributes_require_a_following_title() {
2091 let doc = Parser::default().parse("[#docid]\n[reftext=\"Stuff\"]\n\nBody.");
2095 let header = doc.header();
2096
2097 assert_eq!(header.title(), None);
2098 assert_eq!(header.id(), None);
2099 assert_eq!(doc.attribute_value("reftext"), InterpretedValue::Unset);
2100 }
2101
2102 #[test]
2103 fn stacked_block_attributes_stop_at_a_block_anchor() {
2104 let doc = Parser::default().parse("[#docid]\n[[anchor]]\n= Some Title");
2109 let header = doc.header();
2110
2111 assert_eq!(header.title(), None);
2112 assert_eq!(header.id(), None);
2113 }
2114
2115 #[test]
2116 fn rejected_metadata_run_does_not_fire_counter() {
2117 let doc = Parser::default()
2130 .parse("[reftext=\"See {counter:item}\"]\n[[anchor]]\n= Title\n\n{counter:item}");
2131
2132 assert_eq!(doc.header().title(), None);
2133 assert_eq!(rendered_paragraphs(&doc), vec!["= Title", "2"]);
2134 }
2135
2136 #[test]
2137 fn role_block_attribute_above_title() {
2138 let doc = Parser::default().parse("[role=special]\n= Document Title\n\nBody.");
2143 let header = doc.header();
2144
2145 assert_eq!(header.title(), Some("Document Title"));
2146 assert_eq!(
2147 doc.attribute_value("role"),
2148 InterpretedValue::Value("special")
2149 );
2150 assert_eq!(header.roles(), vec!["special"]);
2151 assert_eq!(doc.roles(), vec!["special"]);
2152
2153 let doc = Parser::default().parse("[.one.two]\n= Document Title");
2155 assert_eq!(
2156 doc.attribute_value("role"),
2157 InterpretedValue::Value("one two")
2158 );
2159 assert_eq!(doc.header().roles(), vec!["one", "two"]);
2160 assert_eq!(doc.roles(), vec!["one", "two"]);
2161 }
2162
2163 #[test]
2164 fn roles_empty_without_block_attribute() {
2165 let doc = Parser::default().parse("= Document Title\n\nBody.");
2168
2169 assert!(doc.header().roles().is_empty());
2170 assert!(doc.roles().is_empty());
2171 }
2172
2173 #[test]
2174 fn options_block_attribute_above_title() {
2175 let doc = Parser::default().parse("[opts=\"noheader,autowidth\"]\n= Document Title");
2178
2179 assert!(doc.is_attribute_set("noheader-option"));
2180 assert!(doc.is_attribute_set("autowidth-option"));
2181
2182 let doc = Parser::default().parse("[%hardbreaks]\n= Document Title");
2184 assert!(doc.is_attribute_set("hardbreaks-option"));
2185 }
2186
2187 #[test]
2188 fn bracketed_line_that_is_not_a_separator_attribute_list() {
2189 let doc = Parser::default().parse("[[anchor]]\n= Some Title: Subtitle");
2195 let header = doc.header();
2196
2197 assert_eq!(header.title(), None);
2198 assert_eq!(header.subtitle(), None);
2199
2200 let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
2201 let header = doc.header();
2202
2203 assert_eq!(header.title(), None);
2204 assert_eq!(header.subtitle(), None);
2205 }
2206
2207 #[test]
2208 fn empty_title_separator_falls_back_to_default() {
2209 let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
2212 let header = doc.header();
2213
2214 assert_eq!(header.main_title(), Some("Main Title"));
2215 assert_eq!(header.subtitle(), Some("Subtitle"));
2216 }
2217
2218 #[test]
2219 fn counter_does_not_shadow_title_separator() {
2220 let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
2226 let header = doc.header();
2227
2228 assert_eq!(header.main_title(), Some("Main Title"));
2229 assert_eq!(header.subtitle(), Some("Subtitle 1"));
2230 }
2231
2232 #[test]
2233 fn skips_block_comment_before_author() {
2234 let doc = Parser::default()
2238 .parse("= Title\n////\nAsciidoctor\nrelease artist\n////\nRyan Waldron");
2239 let header = doc.header();
2240
2241 let author = header.authors().first().unwrap();
2242 assert_eq!(author.name(), "Ryan Waldron");
2243
2244 assert_eq!(header.comments().count(), 1);
2245 assert_eq!(
2246 header.comments().next().unwrap().data(),
2247 "////\nAsciidoctor\nrelease artist\n////"
2248 );
2249 }
2250
2251 #[test]
2252 fn skips_block_comment_with_blank_lines() {
2253 let doc = Parser::default().parse("= Title\n////\n\nAsciidoctor\n\n////\nRyan Waldron");
2256 let header = doc.header();
2257
2258 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2259 assert_eq!(header.comments().count(), 1);
2260 }
2261
2262 #[test]
2263 fn unterminated_block_comment_consumes_rest_of_header() {
2264 let doc = Parser::default().parse("= Title\n////\nAsciidoctor\nRyan Waldron");
2267 let header = doc.header();
2268
2269 assert!(header.authors().is_empty());
2270 assert_eq!(header.comments().count(), 1);
2271 }
2272
2273 #[test]
2274 fn longer_block_comment_delimiter_requires_matching_close() {
2275 let doc = Parser::default()
2279 .parse("= Title\n/////\nAsciidoctor\n////\nstill comment\n/////\nRyan Waldron");
2280 let header = doc.header();
2281
2282 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2283 assert_eq!(header.comments().count(), 1);
2284 }
2285
2286 #[test]
2287 fn three_slashes_is_not_a_block_comment() {
2288 let mut parser = Parser::default();
2294 parser.parse("= Title\nJoe Cool\nv1.0\n///\nstuff");
2295
2296 assert_eq!(
2297 parser.attribute_value("author"),
2298 InterpretedValue::Value("Joe Cool")
2299 );
2300 assert_eq!(
2301 parser.attribute_value("revnumber"),
2302 InterpretedValue::Value("1.0")
2303 );
2304 }
2305
2306 mod markdown_style_document_title {
2307 use crate::tests::prelude::*;
2308
2309 #[test]
2310 fn hash_marker_is_a_document_title() {
2311 let mut parser = Parser::default();
2312 let mi =
2313 crate::document::Header::parse(crate::Span::new("# Just the Title"), &mut parser)
2314 .unwrap_if_no_warnings();
2315
2316 assert_eq!(
2317 mi.item,
2318 Header {
2319 title_source: Some(Span {
2320 data: "Just the Title",
2321 line: 1,
2322 col: 3,
2323 offset: 2,
2324 }),
2325 title: Some("Just the Title"),
2326 attributes: &[],
2327 author_line: None,
2328 revision_line: None,
2329 comments: &[],
2330 source: Span {
2331 data: "# Just the Title",
2332 line: 1,
2333 col: 1,
2334 offset: 0,
2335 }
2336 }
2337 );
2338
2339 assert_eq!(
2340 mi.after,
2341 Span {
2342 data: "",
2343 line: 1,
2344 col: 17,
2345 offset: 16
2346 }
2347 );
2348 }
2349
2350 #[test]
2351 fn sets_doctitle_attribute() {
2352 let doc = Parser::default().parse("# Doc Title\n\n{doctitle}");
2353 assert_eq!(doc.header().title(), Some("Doc Title"));
2354 assert_eq!(rendered_paragraphs(&doc), vec!["Doc Title"]);
2355 }
2356
2357 #[test]
2358 fn strips_symmetric_close() {
2359 let doc = Parser::default().parse("# Doc Title #");
2360 assert_eq!(doc.header().title(), Some("Doc Title"));
2361 }
2362
2363 #[test]
2364 fn does_not_strip_mismatched_close() {
2365 let doc = Parser::default().parse("# Doc Title =");
2368 assert_eq!(doc.header().title(), Some("Doc Title ="));
2369 }
2370
2371 #[test]
2372 fn requires_whitespace_after_marker() {
2373 let doc = Parser::default().parse("#Doc Title");
2374
2375 assert_eq!(doc.header().title(), None);
2376 assert_eq!(rendered_paragraphs(&doc), vec!["#Doc Title"]);
2377 }
2378
2379 #[test]
2380 fn carries_the_rest_of_the_header() {
2381 let doc = Parser::default()
2384 .parse("# Doc Title\n:foo: bar\nKismet R. Lee <kismet@asciidoctor.org>\nv1.0\n");
2385 let header = doc.header();
2386
2387 assert_eq!(header.title(), Some("Doc Title"));
2388 assert_eq!(header.authors().first().unwrap().firstname(), "Kismet");
2389 assert_eq!(header.revision_line().unwrap().revnumber().unwrap(), "1.0");
2390 }
2391
2392 #[test]
2393 fn partitions_subtitle() {
2394 let doc = Parser::default().parse("# Main Title: Subtitle");
2395 let header = doc.header();
2396
2397 assert_eq!(header.main_title(), Some("Main Title"));
2398 assert_eq!(header.subtitle(), Some("Subtitle"));
2399 }
2400
2401 #[test]
2402 fn separator_block_attribute_above_title() {
2403 let doc = Parser::default().parse("[separator=::]\n# Main Title:: Subtitle");
2406 let header = doc.header();
2407
2408 assert_eq!(header.main_title(), Some("Main Title"));
2409 assert_eq!(header.subtitle(), Some("Subtitle"));
2410 }
2411
2412 #[test]
2413 fn markdown_title_followed_by_markdown_sections() {
2414 let doc = Parser::default().parse("# Doc Title\n\n## Section One\n\nblah blah\n");
2415
2416 assert_eq!(doc.header().title(), Some("Doc Title"));
2417
2418 let section = first_section(&doc);
2419
2420 assert_eq!(section.level(), 1);
2421 assert_eq!(section.section_title(), "Section One");
2422 }
2423 }
2424}