1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{Attrlist, AttrlistContext},
4 content::{Content, SubstitutionGroup, substitute_attributes_in_reftext},
5 document::{
6 Attribute, Author, AuthorLine, InterpretedValue, RefType, RevisionLine,
7 matches_author_pattern,
8 },
9 internal::{debug::DebugSliceReference, opaque_iter::opaque_slice_iter},
10 span::MatchedItem,
11 warnings::{MatchAndWarnings, Warning, WarningType},
12};
13
14opaque_slice_iter! {
15 pub struct HeaderAttributes<'a> yielding Attribute<'a>;
18}
19
20opaque_slice_iter! {
21 pub struct Comments<'a> yielding Span<'a>;
24}
25
26#[derive(Clone, Eq, PartialEq)]
30pub struct Header<'src> {
31 title_source: Option<Span<'src>>,
32 title: Option<String>,
33 doctitle: Option<String>,
34 main_title: Option<String>,
35 subtitle: Option<String>,
36 id: Option<String>,
37 roles: Vec<String>,
38 attributes: Vec<Attribute<'src>>,
39 author_line: Option<AuthorLine<'src>>,
40 authors: Vec<Author>,
41 revision_line: Option<RevisionLine<'src>>,
42 comments: Vec<Span<'src>>,
43 source: Span<'src>,
44}
45
46impl<'src> Header<'src> {
47 pub(crate) fn parse(
48 mut source: Span<'src>,
49 parser: &mut Parser,
50 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
51 let original_source = source.discard_empty_lines();
52
53 let mut title_source: Option<Span<'src>> = None;
54 let mut title: Option<String> = None;
55
56 let mut saw_implicit_title = false;
62 let mut implicit_overridden_from_above = false;
63 let mut implicit_doctitle_str: Option<String> = None;
64 let mut doctitle_entry_after_title = false;
65
66 let mut id: Option<String> = None;
67 let mut roles: Vec<String> = vec![];
68 let mut attributes: Vec<Attribute> = vec![];
69 let mut author_line: Option<AuthorLine<'src>> = None;
70 let mut author_attribute: Option<Author> = None;
71 let mut authorinitials_from_entry = false;
72 let mut revision_line: Option<RevisionLine<'src>> = None;
73 let mut comments: Vec<Span<'src>> = vec![];
74 let mut warnings: Vec<Warning<'src>> = vec![];
75
76 while !source.is_empty() {
78 let line_mi = source.take_normalized_line();
79 let line = line_mi.item;
80
81 if line.is_empty() {
83 if title.is_some() {
84 break;
85 }
86 source = line_mi.after;
87 } else if line.starts_with("//") && !line.starts_with("///") {
88 comments.push(line);
89 source = line_mi.after;
90 } else if title.is_some()
91 && let Some((after, terminated)) = skip_block_comment(line, line_mi.after)
92 {
93 comments.push(source.trim_remainder(after).trim_trailing_line_end());
105
106 if !terminated {
112 warnings.push(Warning {
113 source: line,
114 warning: WarningType::UnterminatedDelimitedBlock,
115 origin: None,
116 });
117 }
118
119 source = after;
120 } else if line.starts_with(':')
121 && let Some(attr) = Attribute::parse(source, parser)
122 {
123 if attr
131 .item
132 .name()
133 .data()
134 .eq_ignore_ascii_case("authorinitials")
135 {
136 authorinitials_from_entry =
137 !matches!(attr.item.value(), InterpretedValue::Unset);
138 }
139
140 let mut author_name_override: Option<String> = None;
148 if attr.item.name().data().eq_ignore_ascii_case("author")
149 && let Some(raw_value) = attr.item.raw_value()
150 && let Some(author) = Author::parse(raw_value.data(), parser, true)
151 {
152 parser.set_attribute_by_value_from_header("firstname", author.firstname());
154 if let Some(middlename) = author.middlename() {
155 parser.set_attribute_by_value_from_header("middlename", middlename);
156 }
157 if let Some(lastname) = author.lastname() {
158 parser.set_attribute_by_value_from_header("lastname", lastname);
159 }
160
161 if !authorinitials_from_entry {
164 parser.set_attribute_by_value_from_header(
165 "authorinitials",
166 author.initials(),
167 );
168 }
169
170 if let Some(email) = author.email() {
171 parser.set_attribute_by_value_from_header("email", email);
172 }
173
174 let raw = raw_value.data();
183 if !raw.contains('<') && !raw.contains('{') && !matches_author_pattern(raw) {
184 author_name_override = Some(author.name().to_string());
185 }
186
187 author_attribute = Some(author);
192 }
193
194 parser.set_attribute_from_header(&attr.item, &mut warnings);
195
196 if let Some(author_name) = author_name_override {
197 parser.set_attribute_by_value_from_header("author", author_name);
198 }
199
200 if title.is_some() && attr.item.name().data().eq_ignore_ascii_case("doctitle") {
204 doctitle_entry_after_title = true;
205 }
206
207 attributes.push(attr.item);
208 source = attr.after;
209 } else if title.is_none()
210 && line.starts_with('[')
211 && line.ends_with(']')
212 && document_title_follows_block_metadata(line_mi.after)
213 && let Some((metadata, metadata_warnings)) = parse_document_metadata(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 if let Some(doc_id) = id.as_deref() {
408 let reftext = match parser.attribute_value("reftext") {
409 InterpretedValue::Value(reftext) if !reftext.is_empty() => Some(reftext),
410 _ => doctitle.clone().filter(|title| !title.is_empty()),
411 };
412
413 let _ = parser.register_ref(doc_id, reftext.as_deref(), RefType::Section);
414 }
415
416 let authors = resolve_authors(
422 author_line.as_ref(),
423 author_attribute,
424 !attributes.is_empty(),
425 parser,
426 );
427
428 if !authors.is_empty() {
433 parser.set_attribute_by_value_from_header("authorcount", authors.len().to_string());
434 }
435
436 MatchAndWarnings {
437 item: MatchedItem {
438 item: Self {
439 title_source,
440 title,
441 doctitle,
442 main_title,
443 subtitle,
444 id,
445 roles,
446 attributes,
447 author_line,
448 authors,
449 revision_line,
450 comments,
451 source: source.trim_trailing_whitespace(),
452 },
453 after,
454 },
455 warnings,
456 }
457 }
458
459 pub fn title_source(&'src self) -> Option<Span<'src>> {
461 self.title_source
462 }
463
464 pub fn title(&self) -> Option<&str> {
474 self.title.as_deref()
475 }
476
477 pub(crate) fn doctitle(&self) -> Option<&str> {
489 self.doctitle.as_deref()
490 }
491
492 pub fn main_title(&self) -> Option<&str> {
502 self.main_title.as_deref()
503 }
504
505 pub fn subtitle(&self) -> Option<&str> {
512 self.subtitle.as_deref()
513 }
514
515 pub fn id(&self) -> Option<&str> {
521 self.id.as_deref()
522 }
523
524 pub fn roles(&self) -> Vec<&str> {
533 self.roles.iter().map(String::as_str).collect()
534 }
535
536 pub fn attributes(&'src self) -> HeaderAttributes<'src> {
538 HeaderAttributes::new(&self.attributes)
539 }
540
541 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
543 self.author_line.as_ref()
544 }
545
546 pub fn authors(&self) -> &[Author] {
555 &self.authors
556 }
557
558 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
560 self.revision_line.as_ref()
561 }
562
563 pub fn comments(&'src self) -> Comments<'src> {
565 Comments::new(&self.comments)
566 }
567}
568
569impl<'src> HasSpan<'src> for Header<'src> {
570 fn span(&self) -> Span<'src> {
571 self.source
572 }
573}
574
575fn skip_block_comment<'src>(line: Span<'src>, after: Span<'src>) -> Option<(Span<'src>, bool)> {
590 let delimiter = line.data();
591 if delimiter.len() < 4 || !delimiter.bytes().all(|b| b == b'/') {
592 return None;
593 }
594
595 let mut next = after;
596 let mut terminated = false;
597 while !next.is_empty() {
598 let line_mi = next.take_normalized_line();
599 next = line_mi.after;
600 if line_mi.item.data() == delimiter {
601 terminated = true;
602 break;
603 }
604 }
605
606 Some((next, terminated))
607}
608
609fn document_title_marker(line: Span<'_>) -> Option<char> {
617 if line.starts_with("= ") {
618 Some('=')
619 } else if line.starts_with("# ") {
620 Some('#')
621 } else {
622 None
623 }
624}
625
626fn document_title_follows_block_metadata(after: Span<'_>) -> bool {
642 let mut next = after;
643
644 while !next.is_empty() {
645 let line_mi = next.take_normalized_line();
646 let line = line_mi.item;
647
648 if document_title_marker(line).is_some() {
649 return true;
650 }
651
652 if !is_document_metadata_line(line) {
653 return false;
654 }
655
656 next = line_mi.after;
657 }
658
659 false
660}
661
662fn is_document_metadata_line(line: Span<'_>) -> bool {
672 if !(line.starts_with('[') && line.ends_with(']')) {
673 return false;
674 }
675
676 let inner = line.slice(1..line.len() - 1);
677
678 if inner.is_empty() || inner.starts_with(' ') || inner.starts_with('\t') {
679 return false;
680 }
681
682 if inner.starts_with('[') && inner.ends_with(']') {
686 return inner.len() > 2;
687 }
688
689 true
690}
691
692struct DocumentMetadata {
698 id: Option<String>,
699 separator: Option<String>,
700 reftext: Option<String>,
701 roles: Vec<String>,
702 options: Vec<String>,
703}
704
705fn parse_document_metadata<'src>(
718 line: Span<'src>,
719 parser: &Parser,
720) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
721 if !is_document_metadata_line(line) {
726 return None;
727 }
728
729 let inner = line.slice(1..line.len() - 1);
731
732 if inner.starts_with('[') && inner.ends_with(']') {
734 return parse_document_metadata_anchor(inner.slice(1..inner.len() - 1), parser);
735 }
736
737 let MatchAndWarnings {
738 item: MatchedItem {
739 item: attrlist,
740 after: _,
741 },
742 warnings,
743 } = Attrlist::parse(inner, parser, AttrlistContext::Block);
744
745 let metadata = DocumentMetadata {
746 id: attrlist.id().map(str::to_string),
747 separator: attrlist
748 .named_attribute("separator")
749 .map(|attr| attr.value().to_string()),
750 reftext: attrlist
751 .named_attribute("reftext")
752 .map(|attr| attr.value().to_string()),
753 roles: attrlist.roles().iter().map(|r| r.to_string()).collect(),
754 options: attrlist.options().iter().map(|o| o.to_string()).collect(),
755 };
756
757 Some((metadata, warnings))
758}
759
760fn parse_document_metadata_anchor<'src>(
771 anchor: Span<'src>,
772 parser: &Parser,
773) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
774 let (id, reftext) = match anchor.position(|c| c == ',') {
778 Some(comma) if comma < anchor.len() - 1 => (
779 anchor.slice(0..comma),
780 Some(substitute_attributes_in_reftext(
781 anchor.slice(comma + 1..anchor.len()),
782 parser,
783 )),
784 ),
785 _ => (anchor, None),
786 };
787
788 if !id.is_xml_name() {
789 return None;
790 }
791
792 let metadata = DocumentMetadata {
793 id: Some(id.data().to_string()),
794 separator: None,
795 reftext: reftext.map(|r| r.to_string()),
796 roles: vec![],
797 options: vec![],
798 };
799
800 Some((metadata, vec![]))
801}
802
803fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
811 let separator = match parser.effective_attribute("title-separator") {
816 Some(av) => match &av.value {
817 InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
818 _ => ":".to_string(),
819 },
820 None => ":".to_string(),
821 };
822
823 let separator = format!("{separator} ");
824
825 match title.rfind(&separator) {
826 Some(index) => {
827 let main_title = title[..index].to_string();
828 let subtitle = title[index + separator.len()..].to_string();
829 (main_title, Some(subtitle))
830 }
831 None => (title.to_string(), None),
832 }
833}
834
835fn resolve_authors(
862 author_line: Option<&AuthorLine>,
863 author_attribute: Option<Author>,
864 header_has_attributes: bool,
865 parser: &mut Parser,
866) -> Vec<Author> {
867 if let Some(author_line) = author_line {
868 return author_line.authors().cloned().collect();
869 }
870
871 if !header_has_attributes {
872 return vec![];
873 }
874
875 if attribute_string(parser, "author").is_some()
882 && let Some(author) = author_attribute
883 {
884 return vec![author.with_email(attribute_string(parser, "email"))];
885 }
886
887 if let Some(authors_value) = attribute_string(parser, "authors") {
890 let authors = collect_indexed_authors(
891 split_author_entries(&authors_value)
892 .into_iter()
893 .filter_map(|entry| Author::parse(entry, parser, true)),
894 parser,
895 );
896
897 if !authors.is_empty() {
898 set_author_metadata(parser, &authors);
899 return authors;
900 }
901 }
902
903 let mut raw_names = vec![];
905 let mut index = 1;
906
907 while let Some(name) = attribute_string(parser, &format!("author_{index}")) {
908 raw_names.push(name);
909 index += 1;
910 }
911
912 let authors = collect_indexed_authors(
913 raw_names
914 .iter()
915 .filter_map(|name| Author::parse(name, parser, true)),
916 parser,
917 );
918
919 if !authors.is_empty() {
920 set_author_metadata(parser, &authors);
921 }
922
923 authors
924}
925
926fn attribute_string(parser: &Parser, name: &str) -> Option<String> {
929 match parser.attribute_value(name) {
930 InterpretedValue::Value(value) => Some(value),
931 _ => None,
932 }
933}
934
935fn collect_indexed_authors(authors: impl Iterator<Item = Author>, parser: &Parser) -> Vec<Author> {
939 authors
940 .enumerate()
941 .map(|(idx, author)| {
942 author.with_email(attribute_string(parser, &format!("email_{}", idx + 1)))
943 })
944 .collect()
945}
946
947fn split_author_entries(value: &str) -> Vec<&str> {
954 let bytes = value.as_bytes();
955 let mut entries: Vec<&str> = Vec::new();
956 let mut start = 0;
957
958 for (index, c) in value.char_indices() {
959 if c != ';' {
960 continue;
961 }
962
963 let is_separator = match bytes.get(index + 1) {
964 Some(next) => *next == b' ',
965 None => true,
966 };
967
968 if is_separator {
969 entries.push(&value[start..index]);
970 start = index + 1;
971 }
972 }
973
974 entries.push(&value[start..]);
975 entries
976}
977
978fn set_author_metadata(parser: &mut Parser, authors: &[Author]) {
995 for (idx, author) in authors.iter().enumerate() {
996 set_author_keys(parser, author, if idx == 0 { None } else { Some(idx + 1) });
997
998 if idx == 1
1000 && let Some(first) = authors.first()
1001 {
1002 set_author_keys(parser, first, Some(1));
1003 }
1004 }
1005
1006 let joined = authors
1007 .iter()
1008 .map(Author::name)
1009 .collect::<Vec<_>>()
1010 .join(", ");
1011
1012 parser.set_attribute_by_value_from_header("authors", joined);
1013}
1014
1015fn set_author_keys(parser: &mut Parser, author: &Author, index: Option<usize>) {
1018 let key = |name: &str| match index {
1019 None => name.to_string(),
1020 Some(n) => format!("{name}_{n}"),
1021 };
1022
1023 parser.set_attribute_by_value_from_header(key("author"), author.name());
1024 parser.set_attribute_by_value_from_header(key("firstname"), author.firstname());
1025
1026 if let Some(middlename) = author.middlename() {
1027 parser.set_attribute_by_value_from_header(key("middlename"), middlename);
1028 }
1029
1030 if let Some(lastname) = author.lastname() {
1031 parser.set_attribute_by_value_from_header(key("lastname"), lastname);
1032 }
1033
1034 parser.set_attribute_by_value_from_header(key("authorinitials"), author.initials());
1035
1036 if let Some(email) = author.email() {
1037 parser.set_attribute_by_value_from_header(key("email"), email);
1038 }
1039}
1040
1041fn apply_header_subs(source: &str, parser: &Parser) -> String {
1042 let span = Span::new(source);
1043
1044 let mut content = Content::from(span);
1045 SubstitutionGroup::Header.apply(&mut content, parser, None);
1046
1047 content.rendered().to_string()
1048}
1049
1050impl std::fmt::Debug for Header<'_> {
1051 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1052 f.debug_struct("Header")
1053 .field("title_source", &self.title_source)
1054 .field("title", &self.title)
1055 .field("doctitle", &self.doctitle)
1056 .field("main_title", &self.main_title)
1057 .field("subtitle", &self.subtitle)
1058 .field("id", &self.id)
1059 .field("roles", &self.roles)
1060 .field("attributes", &DebugSliceReference(&self.attributes))
1061 .field("author_line", &self.author_line)
1062 .field("authors", &self.authors)
1063 .field("revision_line", &self.revision_line)
1064 .field("comments", &DebugSliceReference(&self.comments))
1065 .field("source", &self.source)
1066 .finish()
1067 }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 #![allow(clippy::unwrap_used)]
1073
1074 use crate::tests::prelude::*;
1075
1076 #[test]
1077 fn attributes_iterator_supports_exact_size_double_ended_and_nth() {
1078 let doc = Parser::default().parse(":alpha: 1\n:bravo: 2\n:charlie: 3\n\nbody\n");
1082 let header = doc.header();
1083
1084 let names: Vec<_> = header
1086 .attributes()
1087 .map(|a| a.name().data().to_string())
1088 .collect();
1089
1090 assert!(names.len() >= 3);
1091 assert_eq!(names.first().map(String::as_str), Some("alpha"));
1092
1093 assert_eq!(header.attributes().len(), names.len());
1094
1095 assert_eq!(
1096 header.attributes().next_back().map(|a| a.name().data()),
1097 names.last().map(String::as_str),
1098 );
1099
1100 assert_eq!(
1101 header.attributes().nth(1).map(|a| a.name().data()),
1102 Some("bravo"),
1103 );
1104 }
1105
1106 #[test]
1107 fn impl_clone() {
1108 let mut parser = Parser::default();
1110
1111 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
1112 .unwrap_if_no_warnings();
1113 let h2 = h1.clone();
1114
1115 assert_eq!(h1, h2);
1116 }
1117
1118 #[test]
1119 fn only_title() {
1120 let mut parser = Parser::default();
1121 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1122 .unwrap_if_no_warnings();
1123
1124 assert_eq!(
1125 mi.item,
1126 Header {
1127 title_source: Some(Span {
1128 data: "Just the Title",
1129 line: 1,
1130 col: 3,
1131 offset: 2,
1132 }),
1133 title: Some("Just the Title"),
1134 attributes: &[],
1135 author_line: None,
1136 revision_line: None,
1137 comments: &[],
1138 source: Span {
1139 data: "= Just the Title",
1140 line: 1,
1141 col: 1,
1142 offset: 0,
1143 }
1144 }
1145 );
1146
1147 assert_eq!(
1148 mi.after,
1149 Span {
1150 data: "",
1151 line: 1,
1152 col: 17,
1153 offset: 16
1154 }
1155 );
1156 }
1157
1158 #[test]
1159 fn trims_leading_spaces_in_title() {
1160 let mut parser = Parser::default();
1163 let mi =
1164 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
1165 .unwrap_if_no_warnings();
1166
1167 assert_eq!(
1168 mi.item,
1169 Header {
1170 title_source: Some(Span {
1171 data: "Just the Title",
1172 line: 1,
1173 col: 6,
1174 offset: 5,
1175 }),
1176 title: Some("Just the Title"),
1177 attributes: &[],
1178 author_line: None,
1179 revision_line: None,
1180 comments: &[],
1181 source: Span {
1182 data: "= Just the Title",
1183 line: 1,
1184 col: 1,
1185 offset: 0,
1186 }
1187 }
1188 );
1189
1190 assert_eq!(
1191 mi.after,
1192 Span {
1193 data: "",
1194 line: 1,
1195 col: 20,
1196 offset: 19
1197 }
1198 );
1199 }
1200
1201 #[test]
1202 fn trims_trailing_spaces_in_title() {
1203 let mut parser = Parser::default();
1204 let mi =
1205 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
1206 .unwrap_if_no_warnings();
1207
1208 assert_eq!(
1209 mi.item,
1210 Header {
1211 title_source: Some(Span {
1212 data: "Just the Title",
1213 line: 1,
1214 col: 3,
1215 offset: 2,
1216 }),
1217 title: Some("Just the Title"),
1218 attributes: &[],
1219 author_line: None,
1220 revision_line: None,
1221 comments: &[],
1222 source: Span {
1223 data: "= Just the Title",
1224 line: 1,
1225 col: 1,
1226 offset: 0,
1227 }
1228 }
1229 );
1230
1231 assert_eq!(
1232 mi.after,
1233 Span {
1234 data: "",
1235 line: 1,
1236 col: 20,
1237 offset: 19
1238 }
1239 );
1240 }
1241
1242 #[test]
1243 fn title_and_attribute() {
1244 let mut parser = Parser::default();
1245
1246 let mi = crate::document::Header::parse(
1247 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
1248 &mut parser,
1249 )
1250 .unwrap_if_no_warnings();
1251
1252 assert_eq!(
1253 mi.item,
1254 Header {
1255 title_source: Some(Span {
1256 data: "Just the Title",
1257 line: 1,
1258 col: 3,
1259 offset: 2,
1260 }),
1261 title: Some("Just the Title"),
1262 attributes: &[Attribute {
1263 name: Span {
1264 data: "foo",
1265 line: 2,
1266 col: 2,
1267 offset: 18,
1268 },
1269 value_source: Some(Span {
1270 data: "bar",
1271 line: 2,
1272 col: 7,
1273 offset: 23,
1274 }),
1275 value: InterpretedValue::Value("bar"),
1276 source: Span {
1277 data: ":foo: bar",
1278 line: 2,
1279 col: 1,
1280 offset: 17,
1281 }
1282 }],
1283 author_line: None,
1284 revision_line: None,
1285 comments: &[],
1286 source: Span {
1287 data: "= Just the Title\n:foo: bar",
1288 line: 1,
1289 col: 1,
1290 offset: 0,
1291 }
1292 }
1293 );
1294
1295 assert_eq!(
1296 mi.after,
1297 Span {
1298 data: "blah",
1299 line: 4,
1300 col: 1,
1301 offset: 28
1302 }
1303 );
1304 }
1305
1306 #[test]
1307 fn title_applies_header_substitutions() {
1308 let mut parser = Parser::default();
1309
1310 let mi = crate::document::Header::parse(
1311 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
1312 &mut parser,
1313 )
1314 .unwrap_if_no_warnings();
1315
1316 assert_eq!(
1317 mi.item,
1318 Header {
1319 title_source: Some(Span {
1320 data: "The Title & Some{sp}Nonsense",
1321 line: 1,
1322 col: 3,
1323 offset: 2,
1324 }),
1325 title: Some("The Title & Some Nonsense"),
1326 attributes: &[Attribute {
1327 name: Span {
1328 data: "foo",
1329 line: 2,
1330 col: 2,
1331 offset: 32,
1332 },
1333 value_source: Some(Span {
1334 data: "bar",
1335 line: 2,
1336 col: 7,
1337 offset: 37,
1338 }),
1339 value: InterpretedValue::Value("bar"),
1340 source: Span {
1341 data: ":foo: bar",
1342 line: 2,
1343 col: 1,
1344 offset: 31,
1345 }
1346 }],
1347 author_line: None,
1348 revision_line: None,
1349 comments: &[],
1350 source: Span {
1351 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
1352 line: 1,
1353 col: 1,
1354 offset: 0,
1355 }
1356 }
1357 );
1358
1359 assert_eq!(
1360 mi.after,
1361 Span {
1362 data: "blah",
1363 line: 4,
1364 col: 1,
1365 offset: 42
1366 }
1367 );
1368 }
1369
1370 #[test]
1371 fn attribute_without_title() {
1372 let mut parser = Parser::default();
1373 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
1374 .unwrap_if_no_warnings();
1375
1376 assert_eq!(
1377 mi.item,
1378 Header {
1379 title_source: None,
1380 title: None,
1381 attributes: &[Attribute {
1382 name: Span {
1383 data: "foo",
1384 line: 1,
1385 col: 2,
1386 offset: 1,
1387 },
1388 value_source: Some(Span {
1389 data: "bar",
1390 line: 1,
1391 col: 7,
1392 offset: 6,
1393 }),
1394 value: InterpretedValue::Value("bar"),
1395 source: Span {
1396 data: ":foo: bar",
1397 line: 1,
1398 col: 1,
1399 offset: 0,
1400 }
1401 }],
1402 author_line: None,
1403 revision_line: None,
1404 comments: &[],
1405 source: Span {
1406 data: ":foo: bar",
1407 line: 1,
1408 col: 1,
1409 offset: 0,
1410 }
1411 }
1412 );
1413
1414 assert_eq!(
1415 mi.after,
1416 Span {
1417 data: "blah",
1418 line: 3,
1419 col: 1,
1420 offset: 11
1421 }
1422 );
1423 }
1424
1425 #[test]
1426 fn sets_doctitle_attribute() {
1427 let mut parser = Parser::default();
1428 let _doc = parser.parse("= Document Title Goes Here");
1429
1430 assert_eq!(
1431 parser.attribute_value("doctitle"),
1432 InterpretedValue::Value("Document Title Goes Here")
1433 );
1434 }
1435
1436 #[test]
1437 fn sets_author_attributes_from_author_attribute() {
1438 let mut parser = Parser::default();
1439 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
1440
1441 assert_eq!(
1443 parser.attribute_value("firstname"),
1444 InterpretedValue::Value("John")
1445 );
1446 assert_eq!(
1447 parser.attribute_value("middlename"),
1448 InterpretedValue::Value("Q.")
1449 );
1450 assert_eq!(
1451 parser.attribute_value("lastname"),
1452 InterpretedValue::Value("Smith")
1453 );
1454 assert_eq!(
1455 parser.attribute_value("authorinitials"),
1456 InterpretedValue::Value("JQS")
1457 );
1458 assert_eq!(
1459 parser.attribute_value("email"),
1460 InterpretedValue::Value("john@example.com")
1461 );
1462
1463 assert_eq!(
1465 parser.attribute_value("author"),
1466 InterpretedValue::Value("John Q. Smith <john@example.com>")
1467 );
1468 }
1469
1470 #[test]
1471 fn author_attribute_with_four_or_more_parts_is_partitioned() {
1472 let mut parser = Parser::default();
1478 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr.");
1479
1480 assert_eq!(
1481 parser.attribute_value("author"),
1482 InterpretedValue::Value("Leroy Harold Scherer, Jr.")
1483 );
1484 assert_eq!(
1485 parser.attribute_value("firstname"),
1486 InterpretedValue::Value("Leroy")
1487 );
1488 assert_eq!(
1489 parser.attribute_value("middlename"),
1490 InterpretedValue::Value("Harold")
1491 );
1492 assert_eq!(
1493 parser.attribute_value("lastname"),
1494 InterpretedValue::Value("Scherer, Jr.")
1495 );
1496 assert_eq!(
1497 parser.attribute_value("authorinitials"),
1498 InterpretedValue::Value("LHS")
1499 );
1500 }
1501
1502 #[test]
1503 fn author_attribute_two_part_fallback_partitions_lastname() {
1504 let mut parser = Parser::default();
1508 let _doc = parser.parse(":author: Jane, Doe");
1509
1510 assert_eq!(
1511 parser.attribute_value("author"),
1512 InterpretedValue::Value("Jane, Doe")
1513 );
1514 assert_eq!(
1515 parser.attribute_value("firstname"),
1516 InterpretedValue::Value("Jane,")
1517 );
1518 assert_eq!(
1519 parser.attribute_value("middlename"),
1520 InterpretedValue::Unset
1521 );
1522 assert_eq!(
1523 parser.attribute_value("lastname"),
1524 InterpretedValue::Value("Doe")
1525 );
1526 }
1527
1528 #[test]
1529 fn author_attribute_single_part_fallback_is_firstname_only() {
1530 let mut parser = Parser::default();
1533 let _doc = parser.parse(":author: Jane,");
1534
1535 assert_eq!(
1536 parser.attribute_value("author"),
1537 InterpretedValue::Value("Jane,")
1538 );
1539 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!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1548 }
1549
1550 #[test]
1551 fn author_attribute_four_or_more_parts_with_inline_email() {
1552 let mut parser = Parser::default();
1556 let _doc = parser.parse(":author: Leroy Harold Scherer, Jr. <leroy@example.com>");
1557
1558 assert_eq!(
1559 parser.attribute_value("firstname"),
1560 InterpretedValue::Value("Leroy")
1561 );
1562 assert_eq!(
1563 parser.attribute_value("middlename"),
1564 InterpretedValue::Value("Harold")
1565 );
1566 assert_eq!(
1567 parser.attribute_value("lastname"),
1568 InterpretedValue::Value("Scherer, Jr.")
1569 );
1570 assert_eq!(
1571 parser.attribute_value("email"),
1572 InterpretedValue::Value("leroy@example.com")
1573 );
1574 assert_eq!(
1575 parser.attribute_value("authorinitials"),
1576 InterpretedValue::Value("LHS")
1577 );
1578 }
1579
1580 #[test]
1581 fn author_attribute_reference_expands_and_partitions() {
1582 let mut parser = Parser::default();
1586 let _doc = parser.parse(":full-name: Leroy Harold Scherer, Jr.\n:author: {full-name}");
1587
1588 assert_eq!(
1589 parser.attribute_value("firstname"),
1590 InterpretedValue::Value("Leroy")
1591 );
1592 assert_eq!(
1593 parser.attribute_value("middlename"),
1594 InterpretedValue::Value("Harold")
1595 );
1596 assert_eq!(
1597 parser.attribute_value("lastname"),
1598 InterpretedValue::Value("Scherer, Jr.")
1599 );
1600 assert_eq!(
1601 parser.attribute_value("authorinitials"),
1602 InterpretedValue::Value("LHS")
1603 );
1604 }
1605
1606 #[test]
1607 fn author_attribute_reference_within_larger_value_expands_and_partitions() {
1608 let mut parser = Parser::default();
1612 let _doc = parser.parse(":rest: Harold Scherer, Jr.\n:author: Leroy {rest}");
1613
1614 assert_eq!(
1615 parser.attribute_value("firstname"),
1616 InterpretedValue::Value("Leroy")
1617 );
1618 assert_eq!(
1619 parser.attribute_value("middlename"),
1620 InterpretedValue::Value("Harold")
1621 );
1622 assert_eq!(
1623 parser.attribute_value("lastname"),
1624 InterpretedValue::Value("Scherer, Jr.")
1625 );
1626 }
1627
1628 #[test]
1629 fn author_attribute_non_breaking_space_is_not_a_name_separator() {
1630 let mut parser = Parser::default();
1634 let _doc = parser.parse(":author: John\u{a0}Doe Scherer, Jr.");
1635
1636 assert_eq!(
1637 parser.attribute_value("firstname"),
1638 InterpretedValue::Value("John\u{a0}Doe")
1639 );
1640 assert_eq!(
1641 parser.attribute_value("middlename"),
1642 InterpretedValue::Value("Scherer,")
1643 );
1644 assert_eq!(
1645 parser.attribute_value("lastname"),
1646 InterpretedValue::Value("Jr.")
1647 );
1648 }
1649
1650 #[test]
1651 fn sets_author_attributes_from_author_attribute_two_names() {
1652 let mut parser = Parser::default();
1653 let _doc = parser.parse(":author: Jane Doe");
1654
1655 assert_eq!(
1657 parser.attribute_value("firstname"),
1658 InterpretedValue::Value("Jane")
1659 );
1660 assert_eq!(
1661 parser.attribute_value("middlename"),
1662 InterpretedValue::Unset
1663 );
1664 assert_eq!(
1665 parser.attribute_value("lastname"),
1666 InterpretedValue::Value("Doe")
1667 );
1668 assert_eq!(
1669 parser.attribute_value("authorinitials"),
1670 InterpretedValue::Value("JD")
1671 );
1672 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1673 }
1674
1675 #[test]
1676 fn sets_author_attributes_from_author_attribute_single_name() {
1677 let mut parser = Parser::default();
1678 let _doc = parser.parse(":author: Cher");
1679
1680 assert_eq!(
1682 parser.attribute_value("firstname"),
1683 InterpretedValue::Value("Cher")
1684 );
1685 assert_eq!(
1686 parser.attribute_value("middlename"),
1687 InterpretedValue::Unset
1688 );
1689 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1690 assert_eq!(
1691 parser.attribute_value("authorinitials"),
1692 InterpretedValue::Value("C")
1693 );
1694 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1695 }
1696
1697 #[test]
1698 fn sets_author_attributes_from_empty_string() {
1699 let mut parser = Parser::default();
1700 let _doc = parser.parse(":author:");
1701
1702 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
1704 assert_eq!(
1705 parser.attribute_value("middlename"),
1706 InterpretedValue::Unset
1707 );
1708 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
1709 assert_eq!(
1710 parser.attribute_value("authorinitials"),
1711 InterpretedValue::Unset
1712 );
1713 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
1714
1715 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
1716 }
1717
1718 #[test]
1719 fn authors_from_author_line() {
1720 let doc = Parser::default().parse("= Title\nKismet R. Lee <kismet@asciidoctor.org>");
1721
1722 assert_eq!(doc.authors().len(), 1);
1723
1724 let author = doc.authors().first().unwrap();
1725 assert_eq!(author.name(), "Kismet R. Lee");
1726 assert_eq!(author.email(), Some("kismet@asciidoctor.org"));
1727 assert_eq!(author.initials(), "KRL");
1728 }
1729
1730 #[test]
1731 fn authors_from_author_attribute() {
1732 let doc =
1735 Parser::default().parse("= Title\n:author: Jane Q. Public\n:email: jane@example.com");
1736
1737 assert_eq!(doc.authors().len(), 1);
1738
1739 let author = doc.authors().first().unwrap();
1740 assert_eq!(author.name(), "Jane Q. Public");
1741 assert_eq!(author.firstname(), "Jane");
1742 assert_eq!(author.middlename(), Some("Q."));
1743 assert_eq!(author.lastname(), Some("Public"));
1744 assert_eq!(author.email(), Some("jane@example.com"));
1745 assert_eq!(author.initials(), "JQP");
1746 }
1747
1748 #[test]
1749 fn authors_from_author_attribute_with_inline_email() {
1750 let doc = Parser::default().parse("= Title\n:author: John Q. Smith <john@example.com>");
1753
1754 assert_eq!(doc.authors().len(), 1);
1755
1756 let author = doc.authors().first().unwrap();
1757 assert_eq!(author.name(), "John Q. Smith");
1758 assert_eq!(author.firstname(), "John");
1759 assert_eq!(author.middlename(), Some("Q."));
1760 assert_eq!(author.lastname(), Some("Smith"));
1761 assert_eq!(author.email(), Some("john@example.com"));
1762 assert_eq!(author.initials(), "JQS");
1763 }
1764
1765 #[test]
1766 fn authors_is_empty_without_author_info() {
1767 let doc = Parser::default().parse("= Title\n\nBody.");
1768
1769 assert!(doc.authors().is_empty());
1770 }
1771
1772 #[test]
1773 fn authorcount_reflects_author_line() {
1774 let doc = Parser::default().parse("= Title\nJane Doe; John Smith\n\nBody.");
1777
1778 assert_eq!(doc.authors().len(), 2);
1779 assert_eq!(
1780 doc.attribute_value("authorcount"),
1781 InterpretedValue::Value("2")
1782 );
1783
1784 let doc = Parser::default().parse(":author: Jane Doe\n\nBody.");
1786
1787 assert_eq!(
1788 doc.attribute_value("authorcount"),
1789 InterpretedValue::Value("1")
1790 );
1791
1792 let doc = Parser::default().parse("= Title\n\nBody.");
1794
1795 assert_eq!(
1796 doc.attribute_value("authorcount"),
1797 InterpretedValue::Value("0")
1798 );
1799 }
1800
1801 #[test]
1802 fn explicit_authorinitials_after_author_still_wins() {
1803 let doc = Parser::default().parse(":author: Doc Writer\n:authorinitials: DOC\n\nBody.");
1806
1807 assert_eq!(
1808 doc.attribute_value("authorinitials"),
1809 InterpretedValue::Value("DOC")
1810 );
1811
1812 let doc = Parser::default()
1815 .parse(":author: Jane Roe\n:authorinitials: DOC\n:author: Doc Writer\n\nBody.");
1816
1817 assert_eq!(
1818 doc.attribute_value("author"),
1819 InterpretedValue::Value("Doc Writer")
1820 );
1821 assert_eq!(
1822 doc.attribute_value("authorinitials"),
1823 InterpretedValue::Value("DOC")
1824 );
1825 }
1826
1827 #[test]
1828 fn later_author_entry_redrives_initials_without_explicit_override() {
1829 let doc = Parser::default().parse(":author: Jane Roe\n:author: Doc Writer\n\nBody.");
1832
1833 assert_eq!(
1834 doc.attribute_value("authorinitials"),
1835 InterpretedValue::Value("DW")
1836 );
1837 }
1838
1839 #[test]
1840 fn explicit_authorinitials_not_preserved_for_indexed_or_authors_forms() {
1841 let doc = Parser::default().parse(":authorinitials: DOC\n:author_1: Doc Writer\n\nBody.");
1846
1847 assert_eq!(
1848 doc.attribute_value("author"),
1849 InterpretedValue::Value("Doc Writer")
1850 );
1851 assert_eq!(
1852 doc.attribute_value("authorinitials"),
1853 InterpretedValue::Value("DW")
1854 );
1855 }
1856
1857 #[test]
1858 fn authors_attribute_splits_into_indexed_authors() {
1859 let doc = Parser::default().parse(":authors: Jane Doe; John Q. Smith\n\nBody.");
1862
1863 assert_eq!(doc.authors().len(), 2);
1864 assert_eq!(
1865 doc.attribute_value("authors"),
1866 InterpretedValue::Value("Jane Doe, John Q. Smith")
1867 );
1868 assert_eq!(
1869 doc.attribute_value("author"),
1870 InterpretedValue::Value("Jane Doe")
1871 );
1872 assert_eq!(
1873 doc.attribute_value("author_2"),
1874 InterpretedValue::Value("John Q. Smith")
1875 );
1876 assert_eq!(
1877 doc.attribute_value("middlename_2"),
1878 InterpretedValue::Value("Q.")
1879 );
1880 assert_eq!(
1881 doc.attribute_value("authorinitials_2"),
1882 InterpretedValue::Value("JQS")
1883 );
1884 }
1885
1886 #[test]
1887 fn authors_attribute_attaches_companion_emails_and_base_middlename() {
1888 let doc = Parser::default().parse(
1892 ":authors: Jane Q. Doe; John Smith\n:email_1: jane@example.com\n:email_2: john@example.com\n\nBody.",
1893 );
1894
1895 let authors = doc.authors();
1896 assert_eq!(authors.len(), 2);
1897 assert_eq!(authors.first().unwrap().email(), Some("jane@example.com"));
1898 assert_eq!(authors.get(1).unwrap().email(), Some("john@example.com"));
1899
1900 assert_eq!(
1901 doc.attribute_value("middlename"),
1902 InterpretedValue::Value("Q.")
1903 );
1904 assert_eq!(
1905 doc.attribute_value("email"),
1906 InterpretedValue::Value("jane@example.com")
1907 );
1908 assert_eq!(
1909 doc.attribute_value("email_2"),
1910 InterpretedValue::Value("john@example.com")
1911 );
1912 }
1913
1914 #[test]
1915 fn authors_attribute_semicolon_without_space_is_one_author() {
1916 let doc = Parser::default().parse(":authors: Joe Doe;Smith Johnson\n\nBody.");
1919
1920 assert_eq!(doc.authors().len(), 1);
1921 assert_eq!(
1922 doc.attribute_value("authorcount"),
1923 InterpretedValue::Value("1")
1924 );
1925 }
1926
1927 #[test]
1928 fn authors_attribute_single_name_authors_and_trailing_separator() {
1929 let doc = Parser::default().parse(":authors: Cher; Madonna;\n\nBody.");
1932
1933 assert_eq!(doc.authors().len(), 2);
1934 assert_eq!(
1935 doc.attribute_value("authors"),
1936 InterpretedValue::Value("Cher, Madonna")
1937 );
1938 assert_eq!(
1939 doc.attribute_value("author"),
1940 InterpretedValue::Value("Cher")
1941 );
1942 assert_eq!(doc.attribute_value("lastname"), InterpretedValue::Unset);
1943 assert_eq!(
1944 doc.attribute_value("authorinitials"),
1945 InterpretedValue::Value("C")
1946 );
1947 assert_eq!(
1948 doc.attribute_value("author_2"),
1949 InterpretedValue::Value("Madonna")
1950 );
1951 assert_eq!(doc.attribute_value("lastname_2"), InterpretedValue::Unset);
1952 assert_eq!(
1953 doc.attribute_value("authorcount"),
1954 InterpretedValue::Value("2")
1955 );
1956 }
1957
1958 #[test]
1959 fn authors_attribute_with_only_empty_entries_yields_no_authors() {
1960 let doc = Parser::default().parse(":authors: ;\n\nBody.");
1963
1964 assert!(doc.authors().is_empty());
1965 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
1966 assert_eq!(
1967 doc.attribute_value("authorcount"),
1968 InterpretedValue::Value("0")
1969 );
1970
1971 assert_eq!(doc.attribute_value("authors"), InterpretedValue::Value(";"));
1975 }
1976
1977 #[test]
1978 fn author_attribute_takes_precedence_over_authors() {
1979 let doc = Parser::default()
1983 .parse(":author: Solo Writer\n:authors: Jane Doe; John Smith\n\nBody.");
1984
1985 assert_eq!(doc.authors().len(), 1);
1986 assert_eq!(
1987 doc.attribute_value("author"),
1988 InterpretedValue::Value("Solo Writer")
1989 );
1990 assert_eq!(doc.attribute_value("author_2"), InterpretedValue::Unset);
1991 }
1992
1993 #[test]
1994 fn author_unset_after_being_assigned_yields_no_authors() {
1995 let doc = Parser::default().parse("= Title\n:author: Jane Doe\n:author!:\n\nBody.");
1998
1999 assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
2000 assert!(doc.authors().is_empty());
2001 }
2002
2003 #[test]
2004 fn impl_debug() {
2005 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
2006 let header = doc.header();
2007
2008 assert_eq!(
2009 format!("{header:#?}"),
2010 r#"Header {
2011 title_source: Some(
2012 Span {
2013 data: "Example Title",
2014 line: 1,
2015 col: 3,
2016 offset: 2,
2017 },
2018 ),
2019 title: Some(
2020 "Example Title",
2021 ),
2022 doctitle: Some(
2023 "Example Title",
2024 ),
2025 main_title: Some(
2026 "Example Title",
2027 ),
2028 subtitle: None,
2029 id: None,
2030 roles: [],
2031 attributes: &[],
2032 author_line: None,
2033 authors: [],
2034 revision_line: None,
2035 comments: &[],
2036 source: Span {
2037 data: "= Example Title",
2038 line: 1,
2039 col: 1,
2040 offset: 0,
2041 },
2042}"#
2043 );
2044 }
2045
2046 #[test]
2047 fn no_subtitle() {
2048 let doc = Parser::default().parse("= Just the Title");
2051 let header = doc.header();
2052
2053 assert_eq!(header.title(), Some("Just the Title"));
2054 assert_eq!(header.main_title(), Some("Just the Title"));
2055 assert_eq!(header.subtitle(), None);
2056 }
2057
2058 #[test]
2059 fn no_title() {
2060 let doc = Parser::default().parse(":foo: bar\n\nbody");
2062 let header = doc.header();
2063
2064 assert_eq!(header.title(), None);
2065 assert_eq!(header.main_title(), None);
2066 assert_eq!(header.subtitle(), None);
2067 }
2068
2069 #[test]
2070 fn colon_without_space_is_not_a_separator() {
2071 let doc = Parser::default().parse("= Ratio 3:1 Explained");
2074 let header = doc.header();
2075
2076 assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
2077 assert_eq!(header.subtitle(), None);
2078 }
2079
2080 #[test]
2081 fn subtitle_available_on_document() {
2082 let doc = Parser::default().parse("= Main Title: Subtitle");
2085
2086 assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
2087 assert_eq!(doc.subtitle(), Some("Subtitle"));
2088 }
2089
2090 #[test]
2091 fn separator_block_attribute_above_title() {
2092 let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
2095 let header = doc.header();
2096
2097 assert_eq!(header.main_title(), Some("Main Title"));
2098 assert_eq!(header.subtitle(), Some("Subtitle"));
2099
2100 let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
2103 let header = doc.header();
2104
2105 assert_eq!(header.main_title(), Some("Main: Title"));
2106 assert_eq!(header.subtitle(), Some("Subtitle"));
2107 }
2108
2109 #[test]
2110 fn separator_attribute_entry_overrides_block_attribute() {
2111 let doc = Parser::default()
2114 .parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
2115 let header = doc.header();
2116
2117 assert_eq!(header.main_title(), Some("Main Title"));
2118 assert_eq!(header.subtitle(), Some("Subtitle"));
2119 }
2120
2121 #[test]
2122 fn unrecognized_block_attribute_above_title_is_consumed() {
2123 let doc = Parser::default().parse("[foo=bar]\n= A Header Title");
2129 let header = doc.header();
2130
2131 assert_eq!(header.title(), Some("A Header Title"));
2132 assert_eq!(header.subtitle(), None);
2133 assert_eq!(doc.attribute_value("foo"), InterpretedValue::Unset);
2134 }
2135
2136 #[test]
2137 fn reftext_block_attribute_above_title() {
2138 let doc =
2142 Parser::default().parse("[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2143 let header = doc.header();
2144
2145 assert_eq!(header.title(), Some("Links & Stuff"));
2146 assert_eq!(
2147 doc.attribute_value("reftext"),
2148 InterpretedValue::Value("Links and Stuff")
2149 );
2150 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2151 }
2152
2153 #[test]
2154 fn id_block_attribute_above_title() {
2155 let doc = Parser::default().parse("[#docid]\n= Document Title\n\nBody.");
2158 let header = doc.header();
2159
2160 assert_eq!(header.title(), Some("Document Title"));
2161 assert_eq!(header.id(), Some("docid"));
2162 assert_eq!(doc.id(), Some("docid"));
2163
2164 let doc = Parser::default().parse("[id=docid]\n= Document Title");
2166 assert_eq!(doc.header().id(), Some("docid"));
2167 }
2168
2169 #[test]
2170 fn bracket_anchor_above_title() {
2171 let doc = Parser::default().parse("[[idname]]\n= Document Title\n\ncontent");
2174 let header = doc.header();
2175
2176 assert_eq!(header.title(), Some("Document Title"));
2177 assert_eq!(header.id(), Some("idname"));
2178 assert_eq!(doc.id(), Some("idname"));
2179 assert_eq!(rendered_paragraphs(&doc), vec!["content"]);
2180
2181 let doc = Parser::default()
2185 .parse(":product: Widgets\n[[guide,{product} Guide]]\n= User Guide\n\ncontent");
2186 let header = doc.header();
2187
2188 assert_eq!(header.title(), Some("User Guide"));
2189 assert_eq!(header.id(), Some("guide"));
2190 assert_eq!(
2191 doc.attribute_value("reftext"),
2192 InterpretedValue::Value("Widgets Guide")
2193 );
2194 }
2195
2196 #[test]
2197 fn bracket_anchor_above_title_requires_a_valid_name() {
2198 let doc = Parser::default().parse("[[bad name]]\n= Document Title\n\ncontent");
2202 let header = doc.header();
2203
2204 assert_eq!(header.title(), None);
2205 assert_eq!(header.id(), None);
2206 }
2207
2208 #[test]
2209 fn stacked_block_attributes_above_title() {
2210 let doc = Parser::default()
2215 .parse("[#docid]\n[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
2216 let header = doc.header();
2217
2218 assert_eq!(header.title(), Some("Links & Stuff"));
2219 assert_eq!(header.id(), Some("docid"));
2220 assert_eq!(doc.id(), Some("docid"));
2221 assert_eq!(
2222 doc.attribute_value("reftext"),
2223 InterpretedValue::Value("Links and Stuff")
2224 );
2225 assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
2226 }
2227
2228 #[test]
2229 fn stacked_block_attributes_combine_roles() {
2230 let doc = Parser::default().parse("[#docid]\n[.one]\n[.two]\n= Document Title");
2234 let header = doc.header();
2235
2236 assert_eq!(header.title(), Some("Document Title"));
2237 assert_eq!(header.id(), Some("docid"));
2238 assert_eq!(
2239 doc.attribute_value("role"),
2240 InterpretedValue::Value("one two")
2241 );
2242 assert_eq!(header.roles(), vec!["one", "two"]);
2243 }
2244
2245 #[test]
2246 fn stacked_block_attributes_require_a_following_title() {
2247 let doc = Parser::default().parse("[#docid]\n[reftext=\"Stuff\"]\n\nBody.");
2251 let header = doc.header();
2252
2253 assert_eq!(header.title(), None);
2254 assert_eq!(header.id(), None);
2255 assert_eq!(doc.attribute_value("reftext"), InterpretedValue::Unset);
2256 }
2257
2258 #[test]
2259 fn stacked_block_attributes_fold_a_block_anchor() {
2260 let doc = Parser::default().parse("[#docid]\n[[anchor]]\n= Some Title");
2265 let header = doc.header();
2266
2267 assert_eq!(header.title(), Some("Some Title"));
2268 assert_eq!(header.id(), Some("anchor"));
2269 }
2270
2271 #[test]
2272 fn rejected_metadata_run_does_not_fire_counter() {
2273 let doc =
2286 Parser::default().parse("[reftext=\"See {counter:item}\"]\nBody.\n\n{counter:item}");
2287
2288 assert_eq!(doc.header().title(), None);
2289 assert_eq!(rendered_paragraphs(&doc), vec!["Body.", "2"]);
2290 }
2291
2292 #[test]
2293 fn role_block_attribute_above_title() {
2294 let doc = Parser::default().parse("[role=special]\n= Document Title\n\nBody.");
2299 let header = doc.header();
2300
2301 assert_eq!(header.title(), Some("Document Title"));
2302 assert_eq!(
2303 doc.attribute_value("role"),
2304 InterpretedValue::Value("special")
2305 );
2306 assert_eq!(header.roles(), vec!["special"]);
2307 assert_eq!(doc.roles(), vec!["special"]);
2308
2309 let doc = Parser::default().parse("[.one.two]\n= Document Title");
2311 assert_eq!(
2312 doc.attribute_value("role"),
2313 InterpretedValue::Value("one two")
2314 );
2315 assert_eq!(doc.header().roles(), vec!["one", "two"]);
2316 assert_eq!(doc.roles(), vec!["one", "two"]);
2317 }
2318
2319 #[test]
2320 fn roles_empty_without_block_attribute() {
2321 let doc = Parser::default().parse("= Document Title\n\nBody.");
2324
2325 assert!(doc.header().roles().is_empty());
2326 assert!(doc.roles().is_empty());
2327 }
2328
2329 #[test]
2330 fn options_block_attribute_above_title() {
2331 let doc = Parser::default().parse("[opts=\"noheader,autowidth\"]\n= Document Title");
2334
2335 assert!(doc.is_attribute_set("noheader-option"));
2336 assert!(doc.is_attribute_set("autowidth-option"));
2337
2338 let doc = Parser::default().parse("[%hardbreaks]\n= Document Title");
2340 assert!(doc.is_attribute_set("hardbreaks-option"));
2341 }
2342
2343 #[test]
2344 fn bracketed_line_that_is_not_a_separator_attribute_list() {
2345 let doc = Parser::default().parse("[[]]\n= Some Title: Subtitle");
2351 let header = doc.header();
2352
2353 assert_eq!(header.title(), None);
2354 assert_eq!(header.subtitle(), None);
2355
2356 let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
2357 let header = doc.header();
2358
2359 assert_eq!(header.title(), None);
2360 assert_eq!(header.subtitle(), None);
2361 }
2362
2363 #[test]
2364 fn empty_title_separator_falls_back_to_default() {
2365 let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
2368 let header = doc.header();
2369
2370 assert_eq!(header.main_title(), Some("Main Title"));
2371 assert_eq!(header.subtitle(), Some("Subtitle"));
2372 }
2373
2374 #[test]
2375 fn counter_does_not_shadow_title_separator() {
2376 let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
2382 let header = doc.header();
2383
2384 assert_eq!(header.main_title(), Some("Main Title"));
2385 assert_eq!(header.subtitle(), Some("Subtitle 1"));
2386 }
2387
2388 #[test]
2389 fn skips_block_comment_before_author() {
2390 let doc = Parser::default()
2394 .parse("= Title\n////\nAsciidoctor\nrelease artist\n////\nRyan Waldron");
2395 let header = doc.header();
2396
2397 let author = header.authors().first().unwrap();
2398 assert_eq!(author.name(), "Ryan Waldron");
2399
2400 assert_eq!(header.comments().count(), 1);
2401 assert_eq!(
2402 header.comments().next().unwrap().data(),
2403 "////\nAsciidoctor\nrelease artist\n////"
2404 );
2405 }
2406
2407 #[test]
2408 fn skips_block_comment_with_blank_lines() {
2409 let doc = Parser::default().parse("= Title\n////\n\nAsciidoctor\n\n////\nRyan Waldron");
2412 let header = doc.header();
2413
2414 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2415 assert_eq!(header.comments().count(), 1);
2416 }
2417
2418 #[test]
2419 fn unterminated_block_comment_consumes_rest_of_header() {
2420 let doc = Parser::default().parse("= Title\n////\nAsciidoctor\nRyan Waldron");
2423 let header = doc.header();
2424
2425 assert!(header.authors().is_empty());
2426 assert_eq!(header.comments().count(), 1);
2427 }
2428
2429 #[test]
2430 fn longer_block_comment_delimiter_requires_matching_close() {
2431 let doc = Parser::default()
2435 .parse("= Title\n/////\nAsciidoctor\n////\nstill comment\n/////\nRyan Waldron");
2436 let header = doc.header();
2437
2438 assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
2439 assert_eq!(header.comments().count(), 1);
2440 }
2441
2442 #[test]
2443 fn three_slashes_is_not_a_block_comment() {
2444 let mut parser = Parser::default();
2450 let _ = parser.parse("= Title\nJoe Cool\nv1.0\n///\nstuff");
2451
2452 assert_eq!(
2453 parser.attribute_value("author"),
2454 InterpretedValue::Value("Joe Cool")
2455 );
2456 assert_eq!(
2457 parser.attribute_value("revnumber"),
2458 InterpretedValue::Value("1.0")
2459 );
2460 }
2461
2462 mod markdown_style_document_title {
2463 use crate::tests::prelude::*;
2464
2465 #[test]
2466 fn hash_marker_is_a_document_title() {
2467 let mut parser = Parser::default();
2468 let mi =
2469 crate::document::Header::parse(crate::Span::new("# Just the Title"), &mut parser)
2470 .unwrap_if_no_warnings();
2471
2472 assert_eq!(
2473 mi.item,
2474 Header {
2475 title_source: Some(Span {
2476 data: "Just the Title",
2477 line: 1,
2478 col: 3,
2479 offset: 2,
2480 }),
2481 title: Some("Just the Title"),
2482 attributes: &[],
2483 author_line: None,
2484 revision_line: None,
2485 comments: &[],
2486 source: Span {
2487 data: "# Just the Title",
2488 line: 1,
2489 col: 1,
2490 offset: 0,
2491 }
2492 }
2493 );
2494
2495 assert_eq!(
2496 mi.after,
2497 Span {
2498 data: "",
2499 line: 1,
2500 col: 17,
2501 offset: 16
2502 }
2503 );
2504 }
2505
2506 #[test]
2507 fn sets_doctitle_attribute() {
2508 let doc = Parser::default().parse("# Doc Title\n\n{doctitle}");
2509 assert_eq!(doc.header().title(), Some("Doc Title"));
2510 assert_eq!(rendered_paragraphs(&doc), vec!["Doc Title"]);
2511 }
2512
2513 #[test]
2514 fn strips_symmetric_close() {
2515 let doc = Parser::default().parse("# Doc Title #");
2516 assert_eq!(doc.header().title(), Some("Doc Title"));
2517 }
2518
2519 #[test]
2520 fn does_not_strip_mismatched_close() {
2521 let doc = Parser::default().parse("# Doc Title =");
2524 assert_eq!(doc.header().title(), Some("Doc Title ="));
2525 }
2526
2527 #[test]
2528 fn requires_whitespace_after_marker() {
2529 let doc = Parser::default().parse("#Doc Title");
2530
2531 assert_eq!(doc.header().title(), None);
2532 assert_eq!(rendered_paragraphs(&doc), vec!["#Doc Title"]);
2533 }
2534
2535 #[test]
2536 fn carries_the_rest_of_the_header() {
2537 let doc = Parser::default()
2540 .parse("# Doc Title\n:foo: bar\nKismet R. Lee <kismet@asciidoctor.org>\nv1.0\n");
2541 let header = doc.header();
2542
2543 assert_eq!(header.title(), Some("Doc Title"));
2544 assert_eq!(header.authors().first().unwrap().firstname(), "Kismet");
2545 assert_eq!(header.revision_line().unwrap().revnumber().unwrap(), "1.0");
2546 }
2547
2548 #[test]
2549 fn partitions_subtitle() {
2550 let doc = Parser::default().parse("# Main Title: Subtitle");
2551 let header = doc.header();
2552
2553 assert_eq!(header.main_title(), Some("Main Title"));
2554 assert_eq!(header.subtitle(), Some("Subtitle"));
2555 }
2556
2557 #[test]
2558 fn separator_block_attribute_above_title() {
2559 let doc = Parser::default().parse("[separator=::]\n# Main Title:: Subtitle");
2562 let header = doc.header();
2563
2564 assert_eq!(header.main_title(), Some("Main Title"));
2565 assert_eq!(header.subtitle(), Some("Subtitle"));
2566 }
2567
2568 #[test]
2569 fn markdown_title_followed_by_markdown_sections() {
2570 let doc = Parser::default().parse("# Doc Title\n\n## Section One\n\nblah blah\n");
2571
2572 assert_eq!(doc.header().title(), Some("Doc Title"));
2573
2574 let section = first_section(&doc);
2575
2576 assert_eq!(section.level(), 1);
2577 assert_eq!(section.section_title(), "Section One");
2578 }
2579 }
2580}