1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::{Attrlist, AttrlistContext},
6 content::{Content, SubstitutionGroup},
7 document::{Attribute, Author, AuthorLine, InterpretedValue, RevisionLine},
8 internal::debug::DebugSliceReference,
9 span::MatchedItem,
10 warnings::{MatchAndWarnings, Warning, WarningType},
11};
12
13#[derive(Clone, Eq, PartialEq)]
17pub struct Header<'src> {
18 title_source: Option<Span<'src>>,
19 title: Option<String>,
20 main_title: Option<String>,
21 subtitle: Option<String>,
22 attributes: Vec<Attribute<'src>>,
23 author_line: Option<AuthorLine<'src>>,
24 revision_line: Option<RevisionLine<'src>>,
25 comments: Vec<Span<'src>>,
26 source: Span<'src>,
27}
28
29impl<'src> Header<'src> {
30 pub(crate) fn parse(
31 mut source: Span<'src>,
32 parser: &mut Parser,
33 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
34 let original_source = source.discard_empty_lines();
35
36 let mut title_source: Option<Span<'src>> = None;
37 let mut title: Option<String> = None;
38 let mut attributes: Vec<Attribute> = vec![];
39 let mut author_line: Option<AuthorLine<'src>> = None;
40 let mut revision_line: Option<RevisionLine<'src>> = None;
41 let mut comments: Vec<Span<'src>> = vec![];
42 let mut warnings: Vec<Warning<'src>> = vec![];
43
44 while !source.is_empty() {
46 let line_mi = source.take_normalized_line();
47 let line = line_mi.item;
48
49 if line.is_empty() {
51 if title.is_some() {
52 break;
53 }
54 source = line_mi.after;
55 } else if line.starts_with("//") && !line.starts_with("///") {
56 comments.push(line);
57 source = line_mi.after;
58 } else if line.starts_with(':')
59 && let Some(attr) = Attribute::parse(source, parser)
60 {
61 if attr.item.name().data().eq_ignore_ascii_case("author")
64 && let Some(raw_value) = attr.item.raw_value()
65 && let Some(author) = Author::parse(raw_value.data(), parser)
66 {
67 parser.set_attribute_by_value_from_header("firstname", author.firstname());
69 if let Some(middlename) = author.middlename() {
70 parser.set_attribute_by_value_from_header("middlename", middlename);
71 }
72 if let Some(lastname) = author.lastname() {
73 parser.set_attribute_by_value_from_header("lastname", lastname);
74 }
75 parser.set_attribute_by_value_from_header("authorinitials", author.initials());
76 if let Some(email) = author.email() {
77 parser.set_attribute_by_value_from_header("email", email);
78 }
79 }
80
81 parser.set_attribute_from_header(&attr.item, &mut warnings);
82 attributes.push(attr.item);
83 source = attr.after;
84 } else if title.is_none()
85 && line.starts_with('[')
86 && line.ends_with(']')
87 && line_mi.after.take_normalized_line().item.starts_with("= ")
88 && let Some((separator, separator_warnings)) =
89 parse_separator_attribute(line, parser)
90 {
91 warnings.extend(separator_warnings);
92 parser.set_attribute_by_value_from_header("title-separator", separator);
102 source = line_mi.after;
103 } else if title.is_none() && line.starts_with("= ") {
104 let title_span = line.discard(2).discard_whitespace();
105 let title_str = apply_header_subs(title_span.data(), parser);
106
107 parser.set_attribute_by_value_from_header("doctitle", &title_str);
108
109 title = Some(title_str);
110 title_source = Some(title_span);
111 source = line_mi.after;
112 } else if title.is_some() && author_line.is_none() {
113 author_line = Some(AuthorLine::parse(line, parser));
114 source = line_mi.after;
115 } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
116 revision_line = Some(RevisionLine::parse(line, parser));
117 source = line_mi.after;
118 } else {
119 if title.is_some() {
120 warnings.push(Warning {
121 source: line,
122 warning: WarningType::DocumentHeaderNotTerminated,
123 origin: None,
124 });
125 }
126 break;
127 }
128 }
129
130 let after = source.discard_empty_lines();
131 let source = original_source.trim_remainder(source);
132
133 let (main_title, subtitle) = match &title {
138 Some(title) => {
139 let (main_title, subtitle) = partition_title(title, parser);
140 (Some(main_title), subtitle)
141 }
142 None => (None, None),
143 };
144
145 MatchAndWarnings {
146 item: MatchedItem {
147 item: Self {
148 title_source,
149 title,
150 main_title,
151 subtitle,
152 attributes,
153 author_line,
154 revision_line,
155 comments,
156 source: source.trim_trailing_whitespace(),
157 },
158 after,
159 },
160 warnings,
161 }
162 }
163
164 pub fn title_source(&'src self) -> Option<Span<'src>> {
166 self.title_source
167 }
168
169 pub fn title(&self) -> Option<&str> {
179 self.title.as_deref()
180 }
181
182 pub fn main_title(&self) -> Option<&str> {
192 self.main_title.as_deref()
193 }
194
195 pub fn subtitle(&self) -> Option<&str> {
202 self.subtitle.as_deref()
203 }
204
205 pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
207 self.attributes.iter()
208 }
209
210 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
212 self.author_line.as_ref()
213 }
214
215 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
217 self.revision_line.as_ref()
218 }
219
220 pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
222 self.comments.iter()
223 }
224}
225
226impl<'src> HasSpan<'src> for Header<'src> {
227 fn span(&self) -> Span<'src> {
228 self.source
229 }
230}
231
232fn parse_separator_attribute<'src>(
243 line: Span<'src>,
244 parser: &Parser,
245) -> Option<(String, Vec<Warning<'src>>)> {
246 let inner = line.slice(1..line.len() - 1);
249
250 if inner.is_empty()
254 || inner.starts_with(' ')
255 || inner.starts_with('\t')
256 || (inner.starts_with('[') && inner.ends_with(']'))
257 {
258 return None;
259 }
260
261 let MatchAndWarnings {
262 item: MatchedItem {
263 item: attrlist,
264 after: _,
265 },
266 warnings,
267 } = Attrlist::parse(inner, parser, AttrlistContext::Block);
268
269 let separator = attrlist
270 .named_attribute("separator")
271 .map(|attr| attr.value().to_string())?;
272
273 Some((separator, warnings))
274}
275
276fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
284 let separator = match parser.effective_attribute("title-separator") {
289 Some(av) => match &av.value {
290 InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
291 _ => ":".to_string(),
292 },
293 None => ":".to_string(),
294 };
295
296 let separator = format!("{separator} ");
297
298 match title.rfind(&separator) {
299 Some(index) => {
300 let main_title = title[..index].to_string();
301 let subtitle = title[index + separator.len()..].to_string();
302 (main_title, Some(subtitle))
303 }
304 None => (title.to_string(), None),
305 }
306}
307
308fn apply_header_subs(source: &str, parser: &Parser) -> String {
309 let span = Span::new(source);
310
311 let mut content = Content::from(span);
312 SubstitutionGroup::Header.apply(&mut content, parser, None);
313
314 content.rendered().to_string()
315}
316
317impl std::fmt::Debug for Header<'_> {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 f.debug_struct("Header")
320 .field("title_source", &self.title_source)
321 .field("title", &self.title)
322 .field("main_title", &self.main_title)
323 .field("subtitle", &self.subtitle)
324 .field("attributes", &DebugSliceReference(&self.attributes))
325 .field("author_line", &self.author_line)
326 .field("revision_line", &self.revision_line)
327 .field("comments", &DebugSliceReference(&self.comments))
328 .field("source", &self.source)
329 .finish()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 #![allow(clippy::unwrap_used)]
336
337 use crate::tests::prelude::*;
338
339 #[test]
340 fn impl_clone() {
341 let mut parser = Parser::default();
343
344 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
345 .unwrap_if_no_warnings();
346 let h2 = h1.clone();
347
348 assert_eq!(h1, h2);
349 }
350
351 #[test]
352 fn only_title() {
353 let mut parser = Parser::default();
354 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
355 .unwrap_if_no_warnings();
356
357 assert_eq!(
358 mi.item,
359 Header {
360 title_source: Some(Span {
361 data: "Just the Title",
362 line: 1,
363 col: 3,
364 offset: 2,
365 }),
366 title: Some("Just the Title"),
367 attributes: &[],
368 author_line: None,
369 revision_line: None,
370 comments: &[],
371 source: Span {
372 data: "= Just the Title",
373 line: 1,
374 col: 1,
375 offset: 0,
376 }
377 }
378 );
379
380 assert_eq!(
381 mi.after,
382 Span {
383 data: "",
384 line: 1,
385 col: 17,
386 offset: 16
387 }
388 );
389 }
390
391 #[test]
392 fn trims_leading_spaces_in_title() {
393 let mut parser = Parser::default();
396 let mi =
397 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
398 .unwrap_if_no_warnings();
399
400 assert_eq!(
401 mi.item,
402 Header {
403 title_source: Some(Span {
404 data: "Just the Title",
405 line: 1,
406 col: 6,
407 offset: 5,
408 }),
409 title: Some("Just the Title"),
410 attributes: &[],
411 author_line: None,
412 revision_line: None,
413 comments: &[],
414 source: Span {
415 data: "= Just the Title",
416 line: 1,
417 col: 1,
418 offset: 0,
419 }
420 }
421 );
422
423 assert_eq!(
424 mi.after,
425 Span {
426 data: "",
427 line: 1,
428 col: 20,
429 offset: 19
430 }
431 );
432 }
433
434 #[test]
435 fn trims_trailing_spaces_in_title() {
436 let mut parser = Parser::default();
437 let mi =
438 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
439 .unwrap_if_no_warnings();
440
441 assert_eq!(
442 mi.item,
443 Header {
444 title_source: Some(Span {
445 data: "Just the Title",
446 line: 1,
447 col: 3,
448 offset: 2,
449 }),
450 title: Some("Just the Title"),
451 attributes: &[],
452 author_line: None,
453 revision_line: None,
454 comments: &[],
455 source: Span {
456 data: "= Just the Title",
457 line: 1,
458 col: 1,
459 offset: 0,
460 }
461 }
462 );
463
464 assert_eq!(
465 mi.after,
466 Span {
467 data: "",
468 line: 1,
469 col: 20,
470 offset: 19
471 }
472 );
473 }
474
475 #[test]
476 fn title_and_attribute() {
477 let mut parser = Parser::default();
478
479 let mi = crate::document::Header::parse(
480 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
481 &mut parser,
482 )
483 .unwrap_if_no_warnings();
484
485 assert_eq!(
486 mi.item,
487 Header {
488 title_source: Some(Span {
489 data: "Just the Title",
490 line: 1,
491 col: 3,
492 offset: 2,
493 }),
494 title: Some("Just the Title"),
495 attributes: &[Attribute {
496 name: Span {
497 data: "foo",
498 line: 2,
499 col: 2,
500 offset: 18,
501 },
502 value_source: Some(Span {
503 data: "bar",
504 line: 2,
505 col: 7,
506 offset: 23,
507 }),
508 value: InterpretedValue::Value("bar"),
509 source: Span {
510 data: ":foo: bar",
511 line: 2,
512 col: 1,
513 offset: 17,
514 }
515 }],
516 author_line: None,
517 revision_line: None,
518 comments: &[],
519 source: Span {
520 data: "= Just the Title\n:foo: bar",
521 line: 1,
522 col: 1,
523 offset: 0,
524 }
525 }
526 );
527
528 assert_eq!(
529 mi.after,
530 Span {
531 data: "blah",
532 line: 4,
533 col: 1,
534 offset: 28
535 }
536 );
537 }
538
539 #[test]
540 fn title_applies_header_substitutions() {
541 let mut parser = Parser::default();
542
543 let mi = crate::document::Header::parse(
544 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
545 &mut parser,
546 )
547 .unwrap_if_no_warnings();
548
549 assert_eq!(
550 mi.item,
551 Header {
552 title_source: Some(Span {
553 data: "The Title & Some{sp}Nonsense",
554 line: 1,
555 col: 3,
556 offset: 2,
557 }),
558 title: Some("The Title & Some Nonsense"),
559 attributes: &[Attribute {
560 name: Span {
561 data: "foo",
562 line: 2,
563 col: 2,
564 offset: 32,
565 },
566 value_source: Some(Span {
567 data: "bar",
568 line: 2,
569 col: 7,
570 offset: 37,
571 }),
572 value: InterpretedValue::Value("bar"),
573 source: Span {
574 data: ":foo: bar",
575 line: 2,
576 col: 1,
577 offset: 31,
578 }
579 }],
580 author_line: None,
581 revision_line: None,
582 comments: &[],
583 source: Span {
584 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
585 line: 1,
586 col: 1,
587 offset: 0,
588 }
589 }
590 );
591
592 assert_eq!(
593 mi.after,
594 Span {
595 data: "blah",
596 line: 4,
597 col: 1,
598 offset: 42
599 }
600 );
601 }
602
603 #[test]
604 fn attribute_without_title() {
605 let mut parser = Parser::default();
606 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
607 .unwrap_if_no_warnings();
608
609 assert_eq!(
610 mi.item,
611 Header {
612 title_source: None,
613 title: None,
614 attributes: &[Attribute {
615 name: Span {
616 data: "foo",
617 line: 1,
618 col: 2,
619 offset: 1,
620 },
621 value_source: Some(Span {
622 data: "bar",
623 line: 1,
624 col: 7,
625 offset: 6,
626 }),
627 value: InterpretedValue::Value("bar"),
628 source: Span {
629 data: ":foo: bar",
630 line: 1,
631 col: 1,
632 offset: 0,
633 }
634 }],
635 author_line: None,
636 revision_line: None,
637 comments: &[],
638 source: Span {
639 data: ":foo: bar",
640 line: 1,
641 col: 1,
642 offset: 0,
643 }
644 }
645 );
646
647 assert_eq!(
648 mi.after,
649 Span {
650 data: "blah",
651 line: 3,
652 col: 1,
653 offset: 11
654 }
655 );
656 }
657
658 #[test]
659 fn sets_doctitle_attribute() {
660 let mut parser = Parser::default();
661 let _doc = parser.parse("= Document Title Goes Here");
662
663 assert_eq!(
664 parser.attribute_value("doctitle"),
665 InterpretedValue::Value("Document Title Goes Here")
666 );
667 }
668
669 #[test]
670 fn sets_author_attributes_from_author_attribute() {
671 let mut parser = Parser::default();
672 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
673
674 assert_eq!(
676 parser.attribute_value("firstname"),
677 InterpretedValue::Value("John")
678 );
679 assert_eq!(
680 parser.attribute_value("middlename"),
681 InterpretedValue::Value("Q.")
682 );
683 assert_eq!(
684 parser.attribute_value("lastname"),
685 InterpretedValue::Value("Smith")
686 );
687 assert_eq!(
688 parser.attribute_value("authorinitials"),
689 InterpretedValue::Value("JQS")
690 );
691 assert_eq!(
692 parser.attribute_value("email"),
693 InterpretedValue::Value("john@example.com")
694 );
695
696 assert_eq!(
698 parser.attribute_value("author"),
699 InterpretedValue::Value("John Q. Smith <john@example.com>")
700 );
701 }
702
703 #[test]
704 fn sets_author_attributes_from_author_attribute_two_names() {
705 let mut parser = Parser::default();
706 let _doc = parser.parse(":author: Jane Doe");
707
708 assert_eq!(
710 parser.attribute_value("firstname"),
711 InterpretedValue::Value("Jane")
712 );
713 assert_eq!(
714 parser.attribute_value("middlename"),
715 InterpretedValue::Unset
716 );
717 assert_eq!(
718 parser.attribute_value("lastname"),
719 InterpretedValue::Value("Doe")
720 );
721 assert_eq!(
722 parser.attribute_value("authorinitials"),
723 InterpretedValue::Value("JD")
724 );
725 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
726 }
727
728 #[test]
729 fn sets_author_attributes_from_author_attribute_single_name() {
730 let mut parser = Parser::default();
731 let _doc = parser.parse(":author: Cher");
732
733 assert_eq!(
735 parser.attribute_value("firstname"),
736 InterpretedValue::Value("Cher")
737 );
738 assert_eq!(
739 parser.attribute_value("middlename"),
740 InterpretedValue::Unset
741 );
742 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
743 assert_eq!(
744 parser.attribute_value("authorinitials"),
745 InterpretedValue::Value("C")
746 );
747 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
748 }
749
750 #[test]
751 fn sets_author_attributes_from_empty_string() {
752 let mut parser = Parser::default();
753 let _doc = parser.parse(":author:");
754
755 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
757 assert_eq!(
758 parser.attribute_value("middlename"),
759 InterpretedValue::Unset
760 );
761 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
762 assert_eq!(
763 parser.attribute_value("authorinitials"),
764 InterpretedValue::Unset
765 );
766 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
767
768 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
769 }
770
771 #[test]
772 fn impl_debug() {
773 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
774 let header = doc.header();
775
776 assert_eq!(
777 format!("{header:#?}"),
778 r#"Header {
779 title_source: Some(
780 Span {
781 data: "Example Title",
782 line: 1,
783 col: 3,
784 offset: 2,
785 },
786 ),
787 title: Some(
788 "Example Title",
789 ),
790 main_title: Some(
791 "Example Title",
792 ),
793 subtitle: None,
794 attributes: &[],
795 author_line: None,
796 revision_line: None,
797 comments: &[],
798 source: Span {
799 data: "= Example Title",
800 line: 1,
801 col: 1,
802 offset: 0,
803 },
804}"#
805 );
806 }
807
808 #[test]
809 fn no_subtitle() {
810 let doc = Parser::default().parse("= Just the Title");
813 let header = doc.header();
814
815 assert_eq!(header.title(), Some("Just the Title"));
816 assert_eq!(header.main_title(), Some("Just the Title"));
817 assert_eq!(header.subtitle(), None);
818 }
819
820 #[test]
821 fn no_title() {
822 let doc = Parser::default().parse(":foo: bar\n\nbody");
824 let header = doc.header();
825
826 assert_eq!(header.title(), None);
827 assert_eq!(header.main_title(), None);
828 assert_eq!(header.subtitle(), None);
829 }
830
831 #[test]
832 fn colon_without_space_is_not_a_separator() {
833 let doc = Parser::default().parse("= Ratio 3:1 Explained");
836 let header = doc.header();
837
838 assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
839 assert_eq!(header.subtitle(), None);
840 }
841
842 #[test]
843 fn subtitle_available_on_document() {
844 let doc = Parser::default().parse("= Main Title: Subtitle");
847
848 assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
849 assert_eq!(doc.subtitle(), Some("Subtitle"));
850 }
851
852 #[test]
853 fn separator_block_attribute_above_title() {
854 let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
857 let header = doc.header();
858
859 assert_eq!(header.main_title(), Some("Main Title"));
860 assert_eq!(header.subtitle(), Some("Subtitle"));
861
862 let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
865 let header = doc.header();
866
867 assert_eq!(header.main_title(), Some("Main: Title"));
868 assert_eq!(header.subtitle(), Some("Subtitle"));
869 }
870
871 #[test]
872 fn separator_attribute_entry_overrides_block_attribute() {
873 let doc = Parser::default()
876 .parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
877 let header = doc.header();
878
879 assert_eq!(header.main_title(), Some("Main Title"));
880 assert_eq!(header.subtitle(), Some("Subtitle"));
881 }
882
883 #[test]
884 fn non_separator_block_attribute_terminates_header() {
885 let doc = Parser::default().parse("[foo=bar]\n= Not A Header Title");
888 let header = doc.header();
889
890 assert_eq!(header.title(), None);
891 assert_eq!(header.subtitle(), None);
892 }
893
894 #[test]
895 fn bracketed_line_that_is_not_a_separator_attribute_list() {
896 let doc = Parser::default().parse("[[anchor]]\n= Some Title: Subtitle");
902 let header = doc.header();
903
904 assert_eq!(header.title(), None);
905 assert_eq!(header.subtitle(), None);
906
907 let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
908 let header = doc.header();
909
910 assert_eq!(header.title(), None);
911 assert_eq!(header.subtitle(), None);
912 }
913
914 #[test]
915 fn empty_title_separator_falls_back_to_default() {
916 let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
919 let header = doc.header();
920
921 assert_eq!(header.main_title(), Some("Main Title"));
922 assert_eq!(header.subtitle(), Some("Subtitle"));
923 }
924
925 #[test]
926 fn counter_does_not_shadow_title_separator() {
927 let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
933 let header = doc.header();
934
935 assert_eq!(header.main_title(), Some("Main Title"));
936 assert_eq!(header.subtitle(), Some("Subtitle 1"));
937 }
938}