1use std::{fmt, sync::LazyLock};
2
3use regex::Regex;
4
5use crate::{
6 HasSpan, Parser, Span,
7 attributes::Attrlist,
8 blocks::{
9 Block, ChildBlocks, ContentModel, IsBlock, metadata::BlockMetadata,
10 parse_utils::parse_blocks_until,
11 },
12 content::{
13 Content, SubstitutionGroup, XrefSegment, strip_footnote_marker_spans,
14 substitute_attributes_in_reftext,
15 },
16 document::{InterpretedValue, RefType},
17 internal::debug::DebugSliceReference,
18 parser::XrefSignifier,
19 span::MatchedItem,
20 strings::CowStr,
21 warnings::{Warning, WarningType},
22};
23
24#[derive(Clone, Eq, Hash, PartialEq)]
29pub struct SectionBlock<'src> {
30 level: usize,
31 section_title: Content<'src>,
32 blocks: Vec<Block<'src>>,
33 source: Span<'src>,
34 title_source: Option<Span<'src>>,
35 title: Option<Content<'src>>,
36 anchor: Option<Span<'src>>,
37 anchor_reftext: Option<Span<'src>>,
38 attrlist: Option<Attrlist<'src>>,
39 section_type: SectionType,
40 section_id: Option<String>,
41 caption: Option<String>,
42 section_number: Option<SectionNumber>,
43}
44
45impl<'src> SectionBlock<'src> {
46 pub fn child_blocks(&'src self) -> ChildBlocks<'src> {
54 ChildBlocks::from_slice(&self.blocks)
55 }
56
57 pub(crate) fn parse(
58 metadata: &BlockMetadata<'src>,
59 parser: &mut Parser,
60 warnings: &mut Vec<Warning<'src>>,
61 ) -> Option<MatchedItem<'src, Self>> {
62 let discrete = metadata.is_discrete();
63
64 if !discrete && parser.in_delimited_block {
72 return None;
73 }
74
75 let source = metadata.block_start.discard_empty_lines();
76
77 let level_and_title = parse_title_line(source, parser.level_offset(), discrete, warnings)?;
85
86 let sectids = parser.is_attribute_set("sectids");
89
90 let level = level_and_title.item.0;
91
92 let attr_or_anchor_id = metadata
97 .attrlist
98 .as_ref()
99 .and_then(|a| a.id())
100 .or_else(|| metadata.anchor.as_ref().map(|anchor| anchor.data()));
101
102 let (title_span, embedded_id, embedded_reftext) = if attr_or_anchor_id.is_none() {
108 match_embedded_section_anchor(level_and_title.item.1)
109 } else {
110 (level_and_title.item.1, None, None)
111 };
112
113 let section_type = if discrete {
116 SectionType::Discrete
117 } else if level == 1 {
118 let section_type = if let Some(ref attrlist) = metadata.attrlist
119 && let Some(block_style) = attrlist.block_style()
120 && block_style == "appendix"
121 {
122 SectionType::Appendix
123 } else {
124 SectionType::Normal
125 };
126 parser.topmost_section_type = section_type;
127 section_type
128 } else {
129 parser.topmost_section_type
130 };
131
132 let sectnums_active =
142 parser.is_attribute_set("sectnums") && level <= parser.sectnumlevels && !discrete;
143
144 let is_appendix_root = !discrete && level == 1 && section_type == SectionType::Appendix;
145
146 let has_explicit_reftext = metadata
153 .attrlist
154 .as_ref()
155 .and_then(|a| a.named_attribute("reftext"))
156 .is_some()
157 || metadata.anchor_reftext.is_some()
158 || embedded_reftext.is_some();
159
160 let anchor_reftext = metadata
169 .anchor_reftext
170 .as_ref()
171 .map(|span| substitute_attributes_in_reftext(*span, parser));
172 let embedded_reftext =
173 embedded_reftext.map(|span| substitute_attributes_in_reftext(span, parser));
174
175 let (section_number, caption, xref_signifier) = if is_appendix_root {
176 let letter = parser.counter("appendix-number", Some("A"));
183
184 parser
185 .last_appendix_section_number
186 .assign_next_number(level);
187 parser.last_appendix_section_number.appendix_letter = Some(letter);
188
189 let number = parser.last_appendix_section_number.clone();
190 let caption = appendix_caption(parser, &number);
191
192 let signifier = (!has_explicit_reftext).then(|| XrefSignifier {
196 label: join_signifier(
197 parser.attribute_value("appendix-refsig").as_maybe_str(),
198 &number.to_string(),
199 ),
200 emphasize: true,
201 });
202 let section_number = if sectnums_active { Some(number) } else { None };
203 (section_number, Some(caption), signifier)
204 } else if sectnums_active {
205 let number = parser.assign_section_number(level);
206 let signifier = (!has_explicit_reftext).then(|| XrefSignifier {
207 label: join_signifier(
208 parser.attribute_value("section-refsig").as_maybe_str(),
209 &number.to_string(),
210 ),
211 emphasize: false,
212 });
213 (Some(number), None, signifier)
214 } else {
215 (None, None, None)
216 };
217
218 let mut most_recent_level = level;
219
220 let mut section_title = Content::from(title_span);
239 parser.mark_footnote_spans.set(true);
240 SubstitutionGroup::Title.apply(&mut section_title, parser, metadata.attrlist.as_ref());
241 parser.mark_footnote_spans.set(false);
242
243 let title_reftext = strip_footnote_marker_spans(section_title.rendered());
246
247 section_title.remove_footnote_marker_sentinels();
250
251 let is_bibliography_section = !discrete
258 && metadata
259 .attrlist
260 .as_ref()
261 .and_then(|attrlist| attrlist.block_style())
262 == Some("bibliography");
263
264 let previously_in_bibliography_section = parser.parsing_bibliography_section_body;
265 parser.parsing_bibliography_section_body = is_bibliography_section;
266
267 let no_subsection_style = if !discrete && level == 1 {
274 metadata
275 .attrlist
276 .as_ref()
277 .and_then(|attrlist| attrlist.block_style())
278 .filter(|style| special_section_forbids_subsections(style))
279 } else {
280 None
281 };
282
283 if !discrete && let Some(title) = metadata.title.as_ref() {
292 parser.pending_block_title = Some(title.to_owned_title());
296 }
297
298 let mut maw_blocks = parse_blocks_until(
299 level_and_title.after,
300 |i, parser| {
301 discrete
302 || peer_or_ancestor_section(*i, level, &mut most_recent_level, warnings, parser)
303 },
304 parser,
305 );
306
307 parser.parsing_bibliography_section_body = previously_in_bibliography_section;
308
309 let blocks = maw_blocks.item;
310 let source = metadata.source.trim_remainder(blocks.after);
311
312 if let Some(style) = no_subsection_style {
320 for block in &blocks.item {
321 if let Block::Section(subsection) = block
322 && subsection.section_type() != SectionType::Discrete
323 {
324 warnings.push(Warning {
325 source: subsection
326 .section_title_source()
327 .take_normalized_line()
328 .item,
329 warning: WarningType::SpecialSectionCannotHaveNestedSections(
330 style.to_string(),
331 ),
332 origin: None,
333 });
334 }
335 }
336 }
337
338 let proposed_base_id = generate_section_id(&title_reftext, parser);
339
340 let manual_id = attr_or_anchor_id.or(embedded_id);
343
344 let reftext: CowStr<'_> = metadata
349 .attrlist
350 .as_ref()
351 .and_then(|a| {
352 a.named_attribute("reftext")
353 .map(|a| CowStr::from(a.value()))
354 })
355 .or_else(|| anchor_reftext.clone())
356 .or_else(|| embedded_reftext.clone())
357 .unwrap_or_else(|| CowStr::from(title_reftext.as_str()));
358
359 let section_id = if sectids && manual_id.is_none() {
360 let id = parser.generate_and_register_unique_id(
361 &proposed_base_id,
362 Some(&reftext),
363 RefType::Section,
364 );
365 if let Some(signifier) = xref_signifier {
366 parser.set_ref_signifier(&id, signifier);
367 }
368 Some(id)
369 } else {
370 if let Some(manual_id) = manual_id {
371 match parser.register_ref(manual_id, Some(&reftext), RefType::Section) {
372 Ok(()) => {
373 if let Some(signifier) = xref_signifier {
374 parser.set_ref_signifier(manual_id, signifier);
375 }
376 }
377 Err(_duplicate_error) => {
378 warnings.push(Warning {
379 source: metadata.source.trim_remainder(level_and_title.after),
380 warning: WarningType::DuplicateId(manual_id.to_string()),
381 origin: None,
382 });
383 }
384 }
385 }
386
387 embedded_id.map(str::to_string)
392 };
393
394 if level == 1 && !discrete {
396 parser.topmost_section_type = SectionType::Normal;
397 }
398
399 warnings.append(&mut maw_blocks.warnings);
400
401 Some(MatchedItem {
402 item: Self {
403 level,
404 section_title,
405 blocks: blocks.item,
406 source: source.trim_trailing_whitespace(),
407
408 title_source: if discrete {
411 metadata.title_source
412 } else {
413 None
414 },
415 title: if discrete {
416 metadata.title.clone()
417 } else {
418 None
419 },
420 anchor: metadata.anchor,
421 anchor_reftext: metadata.anchor_reftext,
422 attrlist: metadata.attrlist.clone(),
423 section_type,
424 section_id,
425 caption,
426 section_number,
427 },
428 after: blocks.after,
429 })
430 }
431
432 pub fn level(&self) -> usize {
444 self.level
445 }
446
447 pub fn section_title_source(&self) -> Span<'src> {
449 self.section_title.original()
450 }
451
452 pub fn section_title(&'src self) -> &'src str {
455 self.section_title.rendered()
456 }
457
458 pub fn section_type(&'src self) -> SectionType {
460 self.section_type
461 }
462
463 #[cfg(test)]
467 pub(crate) fn section_id(&'src self) -> Option<&'src str> {
468 self.section_id.as_deref()
469 }
470
471 pub fn section_number(&'src self) -> Option<&'src SectionNumber> {
473 self.section_number.as_ref()
474 }
475
476 pub(crate) fn section_title_deferred_parts(&self) -> Option<(&str, &[XrefSegment])> {
484 self.section_title.deferred_parts()
485 }
486
487 pub(crate) fn set_section_title_rendered(&mut self, rendered: String) {
491 self.section_title.set_rendered(rendered);
492 }
493
494 pub(crate) fn reference_id(&self) -> Option<String> {
502 self.attrlist
503 .as_ref()
504 .and_then(|attrlist| attrlist.id())
505 .map(str::to_string)
506 .or_else(|| self.anchor.map(|a| a.data().to_string()))
507 .or_else(|| self.section_id.clone())
508 }
509
510 pub(crate) fn has_explicit_reftext(&self) -> bool {
516 self.attrlist
517 .as_ref()
518 .and_then(|attrlist| attrlist.named_attribute("reftext"))
519 .is_some()
520 || self.anchor_reftext.is_some()
521 }
522}
523
524fn appendix_caption(parser: &Parser, number: &SectionNumber) -> String {
532 let letter = number.to_string();
533 match parser.attribute_value("appendix-caption") {
534 InterpretedValue::Value(label) if !label.is_empty() => format!("{label} {letter}: "),
535 _ => format!("{letter}. "),
536 }
537}
538
539fn join_signifier(signifier: Option<&str>, number: &str) -> String {
545 match signifier {
546 Some(signifier) if !signifier.is_empty() => format!("{signifier} {number}"),
547 _ => number.to_string(),
548 }
549}
550
551impl<'src> IsBlock<'src> for SectionBlock<'src> {
552 fn content_model(&self) -> ContentModel {
553 ContentModel::Compound
554 }
555
556 fn raw_context(&self) -> CowStr<'src> {
563 if self.section_type == SectionType::Discrete {
569 "floating_title".into()
570 } else {
571 "section".into()
572 }
573 }
574
575 fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
576 &mut self.blocks
577 }
578
579 fn title_source(&'src self) -> Option<Span<'src>> {
580 self.title_source
581 }
582
583 fn title(&self) -> Option<&str> {
584 self.title.as_ref().map(Content::rendered_str)
585 }
586
587 fn anchor(&'src self) -> Option<Span<'src>> {
588 self.anchor
589 }
590
591 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
592 self.anchor_reftext
593 }
594
595 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
596 self.attrlist.as_ref()
597 }
598
599 fn caption(&self) -> Option<&str> {
600 self.caption.as_deref()
601 }
602
603 fn id(&'src self) -> Option<&'src str> {
604 self.attrlist()
610 .and_then(|attrlist| attrlist.id())
611 .or_else(|| self.anchor().map(|a| a.data()))
612 .or(self.section_id.as_deref())
614 }
615}
616
617impl<'src> HasSpan<'src> for SectionBlock<'src> {
618 fn span(&self) -> Span<'src> {
619 self.source
620 }
621}
622
623impl std::fmt::Debug for SectionBlock<'_> {
624 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625 f.debug_struct("SectionBlock")
626 .field("level", &self.level)
627 .field("section_title", &self.section_title)
628 .field("blocks", &DebugSliceReference(&self.blocks))
629 .field("source", &self.source)
630 .field("title_source", &self.title_source)
631 .field("title", &self.title)
632 .field("anchor", &self.anchor)
633 .field("anchor_reftext", &self.anchor_reftext)
634 .field("attrlist", &self.attrlist)
635 .field("section_type", &self.section_type)
636 .field("section_id", &self.section_id)
637 .field("caption", &self.caption)
638 .field("section_number", &self.section_number)
639 .finish()
640 }
641}
642
643const MIN_SECTION_LEVEL: i32 = 1;
647const MAX_SECTION_LEVEL: i32 = 5;
648
649pub(crate) fn strip_symmetric_title_close(title: Span<'_>, marker: char, count: usize) -> Span<'_> {
656 const BLANK: [char; 2] = [' ', '\t'];
660 let close = marker.to_string().repeat(count);
661 match title.data().strip_suffix(&close) {
662 Some(without_close)
663 if without_close.ends_with(BLANK)
664 && !without_close.trim_end_matches(BLANK).is_empty() =>
665 {
666 title.slice_to(..without_close.trim_end_matches(BLANK).len())
667 }
668 _ => title,
669 }
670}
671
672#[allow(clippy::unwrap_used)]
681static EMBEDDED_SECTION_ANCHOR: LazyLock<Regex> = LazyLock::new(|| {
682 Regex::new(
683 r"(?x)
684 ^(.*?) # (1) title text before the anchor
685 [\ \t]+ # blank(s) separating title from anchor
686 (\\)? # (2) optional escape backslash
687 \[\[
688 ( [\p{Alphabetic}_:] [\p{Alphabetic}\p{Nd}_\-:.]* ) # (3) anchor id
689 (?: , [\ \t]* (\S.*) )? # (4) optional reftext (may contain commas)
690 \]\]
691 $",
692 )
693 .unwrap()
694});
695
696fn match_embedded_section_anchor<'src>(
706 title: Span<'src>,
707) -> (Span<'src>, Option<&'src str>, Option<Span<'src>>) {
708 if !title.data().ends_with("]]") {
710 return (title, None, None);
711 }
712
713 let Some(caps) = EMBEDDED_SECTION_ANCHOR.captures(title.data()) else {
714 return (title, None, None);
715 };
716
717 if caps.get(2).is_some() {
719 return (title, None, None);
720 }
721
722 let title_text = caps.get(1).map_or("", |m| m.as_str());
723 let id = caps.get(3).map(|m| m.as_str());
724
725 let reftext = caps
730 .get(4)
731 .map(|m| title.slice(m.start()..m.end()).trim_trailing_whitespace());
732
733 (title.slice_to(..title_text.len()), id, reftext)
734}
735
736fn parse_title_line<'src>(
753 source: Span<'src>,
754 offset: i32,
755 discrete: bool,
756 warnings: &mut Vec<Warning<'src>>,
757) -> Option<MatchedItem<'src, (usize, Span<'src>)>> {
758 let mi = source.take_non_empty_line()?;
759 let mut line = mi.item;
760
761 let mut count = 0;
762
763 let marker_char = if line.starts_with('=') { '=' } else { '#' };
764
765 if marker_char == '=' {
766 while let Some(mi) = line.take_prefix("=") {
767 count += 1;
768 line = mi.after;
769 }
770 } else {
771 while let Some(mi) = line.take_prefix("#") {
772 count += 1;
773 line = mi.after;
774 }
775 }
776
777 if count == 0 {
778 return None;
779 }
780
781 if count > 6 {
782 warnings.push(Warning {
783 source: source.take_normalized_line().item,
784 warning: WarningType::SectionHeadingLevelExceedsMaximum(count - 1),
785 origin: None,
786 });
787
788 return None;
789 }
790
791 let syntactic_level = (count - 1) as i32;
796 let effective_level = syntactic_level.saturating_add(offset);
797
798 if !discrete && syntactic_level == 0 && effective_level < MIN_SECTION_LEVEL {
809 warnings.push(Warning {
810 source: source.take_normalized_line().item,
811 warning: WarningType::Level0SectionHeadingNotSupported,
812 origin: None,
813 });
814
815 return None;
816 }
817
818 let title = line.take_required_whitespace()?;
822
823 let title_span = strip_symmetric_title_close(title.after, marker_char, count);
824
825 let min_level = if discrete { 0 } else { MIN_SECTION_LEVEL };
831
832 let level = if effective_level < min_level {
833 warnings.push(Warning {
834 source: source.take_normalized_line().item,
835 warning: WarningType::SectionHeadingLevelOutOfRange(
836 effective_level,
837 min_level as usize,
838 ),
839 origin: None,
840 });
841 min_level as usize
842 } else if effective_level > MAX_SECTION_LEVEL {
843 warnings.push(Warning {
844 source: source.take_normalized_line().item,
845 warning: WarningType::SectionHeadingLevelOutOfRange(
846 effective_level,
847 MAX_SECTION_LEVEL as usize,
848 ),
849 origin: None,
850 });
851 MAX_SECTION_LEVEL as usize
852 } else {
853 effective_level as usize
854 };
855
856 Some(MatchedItem {
857 item: (level, title_span),
858 after: mi.after,
859 })
860}
861
862fn peer_or_ancestor_section<'src>(
863 source: Span<'src>,
864 level: usize,
865 most_recent_level: &mut usize,
866 warnings: &mut Vec<Warning<'src>>,
867 parser: &Parser,
868) -> bool {
869 let mut temp_parser = Parser::default();
873
874 let level_offset = parser.level_offset();
883 if level_offset != 0 {
884 temp_parser.set_attribute_by_value_from_header("leveloffset", level_offset.to_string());
885 }
886
887 let block_metadata_maw = BlockMetadata::parse(source, &mut temp_parser);
888
889 let block_metadata = block_metadata_maw.item;
890 if block_metadata.is_discrete() {
891 return false;
892 }
893
894 let source_after_metadata = block_metadata.block_start.discard_empty_lines();
902
903 let mut ignored_warnings = vec![];
917
918 if let Some(mi) = parse_title_line(
922 source_after_metadata,
923 parser.level_offset(),
924 false,
925 &mut ignored_warnings,
926 ) {
927 let found_level = mi.item.0;
928
929 if found_level > *most_recent_level + 1 {
930 warnings.push(Warning {
931 source: source.take_normalized_line().item,
932 warning: WarningType::SectionHeadingLevelSkipped(*most_recent_level, found_level),
933 origin: None,
934 });
935 }
936
937 *most_recent_level = found_level;
938
939 found_level <= level
940 } else {
941 false
942 }
943}
944
945fn special_section_forbids_subsections(style: &str) -> bool {
954 matches!(
955 style,
956 "glossary" | "bibliography" | "colophon" | "dedication" | "index"
957 )
958}
959
960pub(crate) fn root_section_sequence_warnings<'src>(blocks: &[Block<'src>]) -> Vec<Warning<'src>> {
975 let mut warnings = vec![];
976 let mut most_recent_level = 0;
977
978 for block in blocks {
979 let Block::Section(section) = block else {
980 continue;
981 };
982
983 if section.section_type() == SectionType::Discrete {
984 continue;
985 }
986
987 let found_level = section.level();
988
989 if found_level > most_recent_level + 1 {
990 warnings.push(Warning {
991 source: section.span().take_normalized_line().item,
992 warning: WarningType::SectionHeadingLevelSkipped(most_recent_level, found_level),
993 origin: None,
994 });
995 }
996
997 most_recent_level = found_level;
998 }
999
1000 warnings
1001}
1002
1003fn generate_section_id(title: &str, parser: &Parser) -> String {
1013 let idprefix = parser
1014 .attribute_value("idprefix")
1015 .as_maybe_str()
1016 .unwrap_or_default()
1017 .to_owned();
1018
1019 let idseparator = parser
1020 .attribute_value("idseparator")
1021 .as_maybe_str()
1022 .unwrap_or_default()
1023 .to_owned();
1024
1025 let mut gen_id = title.to_lowercase().to_owned();
1026
1027 #[allow(clippy::unwrap_used)]
1028 static INVALID_SECTION_ID_CHARS: LazyLock<Regex> = LazyLock::new(|| {
1029 Regex::new(
1030 r"<[^>]+>|<[^&]*>|&(?:[a-z][a-z]+\d{0,2}|#\d{2,5}|#x[\da-f]{2,4});|[^ \w\-.]+",
1031 )
1032 .unwrap()
1033 });
1034
1035 gen_id = INVALID_SECTION_ID_CHARS
1036 .replace_all(&gen_id, "")
1037 .to_string();
1038
1039 let sep = idseparator
1041 .chars()
1042 .next()
1043 .map(|s| s.to_string())
1044 .unwrap_or_default();
1045
1046 gen_id = gen_id.replace([' ', '.', '-'], &sep);
1047
1048 if !sep.is_empty() {
1049 while gen_id.contains(&format!("{}{}", sep, sep)) {
1050 gen_id = gen_id.replace(&format!("{}{}", sep, sep), &sep);
1051 }
1052
1053 if gen_id.ends_with(&sep) {
1054 gen_id.pop();
1055 }
1056
1057 if gen_id.starts_with(&sep) {
1063 gen_id = gen_id[sep.len()..].to_string();
1064 }
1065 }
1066
1067 format!("{idprefix}{gen_id}")
1068}
1069
1070#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
1076pub enum SectionType {
1077 #[default]
1079 Normal,
1080
1081 Appendix,
1083
1084 Discrete,
1087}
1088
1089impl std::fmt::Debug for SectionType {
1090 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1091 match self {
1092 SectionType::Normal => write!(f, "SectionType::Normal"),
1093 SectionType::Appendix => write!(f, "SectionType::Appendix"),
1094 SectionType::Discrete => write!(f, "SectionType::Discrete"),
1095 }
1096 }
1097}
1098
1099#[derive(Clone, Default, Eq, Hash, PartialEq)]
1106pub struct SectionNumber {
1107 pub(crate) section_type: SectionType,
1108 pub(crate) components: Vec<usize>,
1109
1110 pub(crate) appendix_letter: Option<String>,
1116}
1117
1118impl SectionNumber {
1119 pub(crate) fn assign_next_number(&mut self, level: usize) {
1124 self.components.truncate(level);
1126
1127 if self.components.len() < level {
1128 self.components.resize(level, 1);
1129 } else if level > 0
1130 && let Some(component) = self.components.get_mut(level - 1)
1131 {
1132 *component += 1;
1133 }
1134 }
1135
1136 pub fn components(&self) -> &[usize] {
1138 &self.components
1139 }
1140
1141 pub fn appendix_letter(&self) -> Option<&str> {
1147 self.appendix_letter.as_deref()
1148 }
1149}
1150
1151impl fmt::Display for SectionNumber {
1152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1153 f.write_str(
1154 &self
1155 .components
1156 .iter()
1157 .enumerate()
1158 .map(|(index, x)| {
1159 if index == 0 && self.section_type == SectionType::Appendix {
1160 if let Some(letter) = &self.appendix_letter {
1164 letter.clone()
1165 } else {
1166 char::from_u32(b'A' as u32 + (x - 1) as u32)
1167 .unwrap_or('?')
1168 .to_string()
1169 }
1170 } else {
1171 x.to_string()
1172 }
1173 })
1174 .collect::<Vec<String>>()
1175 .join("."),
1176 )
1177 }
1178}
1179
1180impl fmt::Debug for SectionNumber {
1181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1182 f.debug_struct("SectionNumber")
1183 .field("section_type", &self.section_type)
1184 .field("components", &DebugSliceReference(&self.components))
1185 .field("appendix_letter", &self.appendix_letter)
1186 .finish()
1187 }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 #![allow(clippy::panic)]
1193 #![allow(clippy::unwrap_used)]
1194
1195 use crate::{
1196 blocks::{metadata::BlockMetadata, section::SectionType},
1197 tests::prelude::*,
1198 };
1199
1200 #[test]
1201 fn impl_clone() {
1202 let mut parser = Parser::default();
1204 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1205
1206 let b1 = crate::blocks::SectionBlock::parse(
1207 &BlockMetadata::new("== Section Title"),
1208 &mut parser,
1209 &mut warnings,
1210 )
1211 .unwrap();
1212
1213 let b2 = b1.item.clone();
1214 assert_eq!(b1.item, b2);
1215 }
1216
1217 #[test]
1218 fn err_empty_source() {
1219 let mut parser = Parser::default();
1220 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1221
1222 assert!(
1223 crate::blocks::SectionBlock::parse(&BlockMetadata::new(""), &mut parser, &mut warnings)
1224 .is_none()
1225 );
1226 }
1227
1228 #[test]
1229 fn err_only_spaces() {
1230 let mut parser = Parser::default();
1231 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1232
1233 assert!(
1234 crate::blocks::SectionBlock::parse(
1235 &BlockMetadata::new(" "),
1236 &mut parser,
1237 &mut warnings
1238 )
1239 .is_none()
1240 );
1241 }
1242
1243 #[test]
1244 fn err_not_section() {
1245 let mut parser = Parser::default();
1246 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1247
1248 assert!(
1249 crate::blocks::SectionBlock::parse(
1250 &BlockMetadata::new("blah blah"),
1251 &mut parser,
1252 &mut warnings
1253 )
1254 .is_none()
1255 );
1256 }
1257
1258 mod asciidoc_style_headers {
1259 use std::ops::Deref;
1260
1261 use crate::{
1262 blocks::{ContentModel, MediaType, metadata::BlockMetadata, section::SectionType},
1263 tests::prelude::*,
1264 };
1265
1266 #[test]
1267 fn err_missing_space_before_title() {
1268 let mut parser = Parser::default();
1269 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1270
1271 assert!(
1272 crate::blocks::SectionBlock::parse(
1273 &BlockMetadata::new("=blah blah"),
1274 &mut parser,
1275 &mut warnings
1276 )
1277 .is_none()
1278 );
1279 }
1280
1281 #[test]
1282 fn simplest_section_block() {
1283 let mut parser = Parser::default();
1284 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1285
1286 let mi = crate::blocks::SectionBlock::parse(
1287 &BlockMetadata::new("== Section Title"),
1288 &mut parser,
1289 &mut warnings,
1290 )
1291 .unwrap();
1292
1293 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1294 assert_eq!(mi.item.raw_context().deref(), "section");
1295 assert_eq!(mi.item.resolved_context().deref(), "section");
1296 assert!(mi.item.declared_style().is_none());
1297 assert_eq!(mi.item.id().unwrap(), "_section_title");
1298 assert!(mi.item.roles().is_empty());
1299 assert!(mi.item.options().is_empty());
1300 assert!(mi.item.title_source().is_none());
1301 assert!(mi.item.title().is_none());
1302 assert!(mi.item.anchor().is_none());
1303 assert!(mi.item.anchor_reftext().is_none());
1304 assert!(mi.item.attrlist().is_none());
1305 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1306
1307 assert_eq!(
1308 mi.item,
1309 SectionBlock {
1310 level: 1,
1311 section_title: Content {
1312 original: Span {
1313 data: "Section Title",
1314 line: 1,
1315 col: 4,
1316 offset: 3,
1317 },
1318 rendered: "Section Title",
1319 },
1320 blocks: &[],
1321 source: Span {
1322 data: "== Section Title",
1323 line: 1,
1324 col: 1,
1325 offset: 0,
1326 },
1327 title_source: None,
1328 title: None,
1329 anchor: None,
1330 anchor_reftext: None,
1331 attrlist: None,
1332 section_type: SectionType::Normal,
1333 section_id: Some("_section_title"),
1334 caption: None,
1335 section_number: None,
1336 }
1337 );
1338
1339 assert_eq!(
1340 mi.after,
1341 Span {
1342 data: "",
1343 line: 1,
1344 col: 17,
1345 offset: 16
1346 }
1347 );
1348 }
1349
1350 #[test]
1351 fn has_child_block() {
1352 let mut parser = Parser::default();
1353 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1354
1355 let mi = crate::blocks::SectionBlock::parse(
1356 &BlockMetadata::new("== Section Title\n\nabc"),
1357 &mut parser,
1358 &mut warnings,
1359 )
1360 .unwrap();
1361
1362 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1363 assert_eq!(mi.item.raw_context().deref(), "section");
1364 assert_eq!(mi.item.resolved_context().deref(), "section");
1365 assert!(mi.item.declared_style().is_none());
1366 assert_eq!(mi.item.id().unwrap(), "_section_title");
1367 assert!(mi.item.roles().is_empty());
1368 assert!(mi.item.options().is_empty());
1369 assert!(mi.item.title_source().is_none());
1370 assert!(mi.item.title().is_none());
1371 assert!(mi.item.anchor().is_none());
1372 assert!(mi.item.anchor_reftext().is_none());
1373 assert!(mi.item.attrlist().is_none());
1374 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1375
1376 assert_eq!(
1377 mi.item,
1378 SectionBlock {
1379 level: 1,
1380 section_title: Content {
1381 original: Span {
1382 data: "Section Title",
1383 line: 1,
1384 col: 4,
1385 offset: 3,
1386 },
1387 rendered: "Section Title",
1388 },
1389 blocks: &[Block::Simple(SimpleBlock {
1390 content: Content {
1391 original: Span {
1392 data: "abc",
1393 line: 3,
1394 col: 1,
1395 offset: 18,
1396 },
1397 rendered: "abc",
1398 },
1399 source: Span {
1400 data: "abc",
1401 line: 3,
1402 col: 1,
1403 offset: 18,
1404 },
1405 style: SimpleBlockStyle::Paragraph,
1406 title_source: None,
1407 title: None,
1408 caption: None,
1409 number: None,
1410 anchor: None,
1411 anchor_reftext: None,
1412 attrlist: None,
1413 })],
1414 source: Span {
1415 data: "== Section Title\n\nabc",
1416 line: 1,
1417 col: 1,
1418 offset: 0,
1419 },
1420 title_source: None,
1421 title: None,
1422 anchor: None,
1423 anchor_reftext: None,
1424 attrlist: None,
1425 section_type: SectionType::Normal,
1426 section_id: Some("_section_title"),
1427 caption: None,
1428 section_number: None,
1429 }
1430 );
1431
1432 assert_eq!(
1433 mi.after,
1434 Span {
1435 data: "",
1436 line: 3,
1437 col: 4,
1438 offset: 21
1439 }
1440 );
1441 }
1442
1443 #[test]
1444 fn has_macro_block_with_extra_blank_line() {
1445 let mut parser = Parser::default();
1446 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1447
1448 let mi = crate::blocks::SectionBlock::parse(
1449 &BlockMetadata::new(
1450 "== Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]\n\n",
1451 ),
1452 &mut parser,
1453 &mut warnings,
1454 )
1455 .unwrap();
1456
1457 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1458 assert_eq!(mi.item.raw_context().deref(), "section");
1459 assert_eq!(mi.item.resolved_context().deref(), "section");
1460 assert!(mi.item.declared_style().is_none());
1461 assert_eq!(mi.item.id().unwrap(), "_section_title");
1462 assert!(mi.item.roles().is_empty());
1463 assert!(mi.item.options().is_empty());
1464 assert!(mi.item.title_source().is_none());
1465 assert!(mi.item.title().is_none());
1466 assert!(mi.item.anchor().is_none());
1467 assert!(mi.item.anchor_reftext().is_none());
1468 assert!(mi.item.attrlist().is_none());
1469 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1470
1471 assert_eq!(
1472 mi.item,
1473 SectionBlock {
1474 level: 1,
1475 section_title: Content {
1476 original: Span {
1477 data: "Section Title",
1478 line: 1,
1479 col: 4,
1480 offset: 3,
1481 },
1482 rendered: "Section Title",
1483 },
1484 blocks: &[Block::Media(MediaBlock {
1485 type_: MediaType::Image,
1486 target: Span {
1487 data: "bar",
1488 line: 3,
1489 col: 8,
1490 offset: 25,
1491 },
1492 macro_attrlist: Attrlist {
1493 attributes: &[
1494 ElementAttribute {
1495 name: Some("alt"),
1496 shorthand_items: &[],
1497 value: "Sunset"
1498 },
1499 ElementAttribute {
1500 name: Some("width"),
1501 shorthand_items: &[],
1502 value: "300"
1503 },
1504 ElementAttribute {
1505 name: Some("height"),
1506 shorthand_items: &[],
1507 value: "400"
1508 }
1509 ],
1510 anchor: None,
1511 source: Span {
1512 data: "alt=Sunset,width=300,height=400",
1513 line: 3,
1514 col: 12,
1515 offset: 29,
1516 }
1517 },
1518 source: Span {
1519 data: "image::bar[alt=Sunset,width=300,height=400]",
1520 line: 3,
1521 col: 1,
1522 offset: 18,
1523 },
1524 title_source: None,
1525 title: None,
1526 caption: None,
1527 number: None,
1528 anchor: None,
1529 anchor_reftext: None,
1530 attrlist: None,
1531 })],
1532 source: Span {
1533 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]",
1534 line: 1,
1535 col: 1,
1536 offset: 0,
1537 },
1538 title_source: None,
1539 title: None,
1540 anchor: None,
1541 anchor_reftext: None,
1542 attrlist: None,
1543 section_type: SectionType::Normal,
1544 section_id: Some("_section_title"),
1545 caption: None,
1546 section_number: None,
1547 }
1548 );
1549
1550 assert_eq!(
1551 mi.after,
1552 Span {
1553 data: "",
1554 line: 5,
1555 col: 1,
1556 offset: 63
1557 }
1558 );
1559 }
1560
1561 #[test]
1562 fn has_child_block_with_errors() {
1563 let mut parser = Parser::default();
1564 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1565
1566 let mi = crate::blocks::SectionBlock::parse(
1567 &BlockMetadata::new(
1568 "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1569 ),
1570 &mut parser,
1571 &mut warnings,
1572 )
1573 .unwrap();
1574
1575 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1576 assert_eq!(mi.item.raw_context().deref(), "section");
1577 assert_eq!(mi.item.resolved_context().deref(), "section");
1578 assert!(mi.item.declared_style().is_none());
1579 assert_eq!(mi.item.id().unwrap(), "_section_title");
1580 assert!(mi.item.roles().is_empty());
1581 assert!(mi.item.options().is_empty());
1582 assert!(mi.item.title_source().is_none());
1583 assert!(mi.item.title().is_none());
1584 assert!(mi.item.anchor().is_none());
1585 assert!(mi.item.anchor_reftext().is_none());
1586 assert!(mi.item.attrlist().is_none());
1587 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1588
1589 assert_eq!(
1590 mi.item,
1591 SectionBlock {
1592 level: 1,
1593 section_title: Content {
1594 original: Span {
1595 data: "Section Title",
1596 line: 1,
1597 col: 4,
1598 offset: 3,
1599 },
1600 rendered: "Section Title",
1601 },
1602 blocks: &[Block::Media(MediaBlock {
1603 type_: MediaType::Image,
1604 target: Span {
1605 data: "bar",
1606 line: 3,
1607 col: 8,
1608 offset: 25,
1609 },
1610 macro_attrlist: Attrlist {
1611 attributes: &[
1612 ElementAttribute {
1613 name: Some("alt"),
1614 shorthand_items: &[],
1615 value: "Sunset"
1616 },
1617 ElementAttribute {
1618 name: Some("width"),
1619 shorthand_items: &[],
1620 value: "300"
1621 },
1622 ElementAttribute {
1623 name: Some("height"),
1624 shorthand_items: &[],
1625 value: "400"
1626 }
1627 ],
1628 anchor: None,
1629 source: Span {
1630 data: "alt=Sunset,width=300,,height=400",
1631 line: 3,
1632 col: 12,
1633 offset: 29,
1634 }
1635 },
1636 source: Span {
1637 data: "image::bar[alt=Sunset,width=300,,height=400]",
1638 line: 3,
1639 col: 1,
1640 offset: 18,
1641 },
1642 title_source: None,
1643 title: None,
1644 caption: None,
1645 number: None,
1646 anchor: None,
1647 anchor_reftext: None,
1648 attrlist: None,
1649 })],
1650 source: Span {
1651 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1652 line: 1,
1653 col: 1,
1654 offset: 0,
1655 },
1656 title_source: None,
1657 title: None,
1658 anchor: None,
1659 anchor_reftext: None,
1660 attrlist: None,
1661 section_type: SectionType::Normal,
1662 section_id: Some("_section_title"),
1663 caption: None,
1664 section_number: None,
1665 }
1666 );
1667
1668 assert_eq!(
1669 mi.after,
1670 Span {
1671 data: "",
1672 line: 3,
1673 col: 45,
1674 offset: 62
1675 }
1676 );
1677
1678 assert_eq!(
1679 warnings,
1680 vec![Warning {
1681 source: Span {
1682 data: "alt=Sunset,width=300,,height=400",
1683 line: 3,
1684 col: 12,
1685 offset: 29,
1686 },
1687 warning: WarningType::EmptyAttributeValue,
1688 }]
1689 );
1690 }
1691
1692 #[test]
1693 fn dont_stop_at_child_section() {
1694 let mut parser = Parser::default();
1695 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1696
1697 let mi = crate::blocks::SectionBlock::parse(
1698 &BlockMetadata::new("== Section Title\n\nabc\n\n=== Section 2\n\ndef"),
1699 &mut parser,
1700 &mut warnings,
1701 )
1702 .unwrap();
1703
1704 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1705 assert_eq!(mi.item.raw_context().deref(), "section");
1706 assert_eq!(mi.item.resolved_context().deref(), "section");
1707 assert!(mi.item.declared_style().is_none());
1708 assert_eq!(mi.item.id().unwrap(), "_section_title");
1709 assert!(mi.item.roles().is_empty());
1710 assert!(mi.item.options().is_empty());
1711 assert!(mi.item.title_source().is_none());
1712 assert!(mi.item.title().is_none());
1713 assert!(mi.item.anchor().is_none());
1714 assert!(mi.item.anchor_reftext().is_none());
1715 assert!(mi.item.attrlist().is_none());
1716 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1717
1718 assert_eq!(
1719 mi.item,
1720 SectionBlock {
1721 level: 1,
1722 section_title: Content {
1723 original: Span {
1724 data: "Section Title",
1725 line: 1,
1726 col: 4,
1727 offset: 3,
1728 },
1729 rendered: "Section Title",
1730 },
1731 blocks: &[
1732 Block::Simple(SimpleBlock {
1733 content: Content {
1734 original: Span {
1735 data: "abc",
1736 line: 3,
1737 col: 1,
1738 offset: 18,
1739 },
1740 rendered: "abc",
1741 },
1742 source: Span {
1743 data: "abc",
1744 line: 3,
1745 col: 1,
1746 offset: 18,
1747 },
1748 style: SimpleBlockStyle::Paragraph,
1749 title_source: None,
1750 title: None,
1751 caption: None,
1752 number: None,
1753 anchor: None,
1754 anchor_reftext: None,
1755 attrlist: None,
1756 }),
1757 Block::Section(SectionBlock {
1758 level: 2,
1759 section_title: Content {
1760 original: Span {
1761 data: "Section 2",
1762 line: 5,
1763 col: 5,
1764 offset: 27,
1765 },
1766 rendered: "Section 2",
1767 },
1768 blocks: &[Block::Simple(SimpleBlock {
1769 content: Content {
1770 original: Span {
1771 data: "def",
1772 line: 7,
1773 col: 1,
1774 offset: 38,
1775 },
1776 rendered: "def",
1777 },
1778 source: Span {
1779 data: "def",
1780 line: 7,
1781 col: 1,
1782 offset: 38,
1783 },
1784 style: SimpleBlockStyle::Paragraph,
1785 title_source: None,
1786 title: None,
1787 caption: None,
1788 number: None,
1789 anchor: None,
1790 anchor_reftext: None,
1791 attrlist: None,
1792 })],
1793 source: Span {
1794 data: "=== Section 2\n\ndef",
1795 line: 5,
1796 col: 1,
1797 offset: 23,
1798 },
1799 title_source: None,
1800 title: None,
1801 anchor: None,
1802 anchor_reftext: None,
1803 attrlist: None,
1804 section_type: SectionType::Normal,
1805 section_id: Some("_section_2"),
1806 caption: None,
1807 section_number: None,
1808 })
1809 ],
1810 source: Span {
1811 data: "== Section Title\n\nabc\n\n=== Section 2\n\ndef",
1812 line: 1,
1813 col: 1,
1814 offset: 0,
1815 },
1816 title_source: None,
1817 title: None,
1818 anchor: None,
1819 anchor_reftext: None,
1820 attrlist: None,
1821 section_type: SectionType::Normal,
1822 section_id: Some("_section_title"),
1823 caption: None,
1824 section_number: None,
1825 }
1826 );
1827
1828 assert_eq!(
1829 mi.after,
1830 Span {
1831 data: "",
1832 line: 7,
1833 col: 4,
1834 offset: 41
1835 }
1836 );
1837 }
1838
1839 #[test]
1840 fn stop_at_peer_section() {
1841 let mut parser = Parser::default();
1842 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1843
1844 let mi = crate::blocks::SectionBlock::parse(
1845 &BlockMetadata::new("== Section Title\n\nabc\n\n== Section 2\n\ndef"),
1846 &mut parser,
1847 &mut warnings,
1848 )
1849 .unwrap();
1850
1851 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1852 assert_eq!(mi.item.raw_context().deref(), "section");
1853 assert_eq!(mi.item.resolved_context().deref(), "section");
1854 assert!(mi.item.declared_style().is_none());
1855 assert_eq!(mi.item.id().unwrap(), "_section_title");
1856 assert!(mi.item.roles().is_empty());
1857 assert!(mi.item.options().is_empty());
1858 assert!(mi.item.title_source().is_none());
1859 assert!(mi.item.title().is_none());
1860 assert!(mi.item.anchor().is_none());
1861 assert!(mi.item.anchor_reftext().is_none());
1862 assert!(mi.item.attrlist().is_none());
1863 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1864
1865 assert_eq!(
1866 mi.item,
1867 SectionBlock {
1868 level: 1,
1869 section_title: Content {
1870 original: Span {
1871 data: "Section Title",
1872 line: 1,
1873 col: 4,
1874 offset: 3,
1875 },
1876 rendered: "Section Title",
1877 },
1878 blocks: &[Block::Simple(SimpleBlock {
1879 content: Content {
1880 original: Span {
1881 data: "abc",
1882 line: 3,
1883 col: 1,
1884 offset: 18,
1885 },
1886 rendered: "abc",
1887 },
1888 source: Span {
1889 data: "abc",
1890 line: 3,
1891 col: 1,
1892 offset: 18,
1893 },
1894 style: SimpleBlockStyle::Paragraph,
1895 title_source: None,
1896 title: None,
1897 caption: None,
1898 number: None,
1899 anchor: None,
1900 anchor_reftext: None,
1901 attrlist: None,
1902 })],
1903 source: Span {
1904 data: "== Section Title\n\nabc",
1905 line: 1,
1906 col: 1,
1907 offset: 0,
1908 },
1909 title_source: None,
1910 title: None,
1911 anchor: None,
1912 anchor_reftext: None,
1913 attrlist: None,
1914 section_type: SectionType::Normal,
1915 section_id: Some("_section_title"),
1916 caption: None,
1917 section_number: None,
1918 }
1919 );
1920
1921 assert_eq!(
1922 mi.after,
1923 Span {
1924 data: "== Section 2\n\ndef",
1925 line: 5,
1926 col: 1,
1927 offset: 23
1928 }
1929 );
1930 }
1931
1932 #[test]
1933 fn stop_at_ancestor_section() {
1934 let mut parser = Parser::default();
1935 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1936
1937 let mi = crate::blocks::SectionBlock::parse(
1938 &BlockMetadata::new("=== Section Title\n\nabc\n\n== Section 2\n\ndef"),
1939 &mut parser,
1940 &mut warnings,
1941 )
1942 .unwrap();
1943
1944 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1945 assert_eq!(mi.item.raw_context().deref(), "section");
1946 assert_eq!(mi.item.resolved_context().deref(), "section");
1947 assert!(mi.item.declared_style().is_none());
1948 assert_eq!(mi.item.id().unwrap(), "_section_title");
1949 assert!(mi.item.roles().is_empty());
1950 assert!(mi.item.options().is_empty());
1951 assert!(mi.item.title_source().is_none());
1952 assert!(mi.item.title().is_none());
1953 assert!(mi.item.anchor().is_none());
1954 assert!(mi.item.anchor_reftext().is_none());
1955 assert!(mi.item.attrlist().is_none());
1956 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1957
1958 assert_eq!(
1959 mi.item,
1960 SectionBlock {
1961 level: 2,
1962 section_title: Content {
1963 original: Span {
1964 data: "Section Title",
1965 line: 1,
1966 col: 5,
1967 offset: 4,
1968 },
1969 rendered: "Section Title",
1970 },
1971 blocks: &[Block::Simple(SimpleBlock {
1972 content: Content {
1973 original: Span {
1974 data: "abc",
1975 line: 3,
1976 col: 1,
1977 offset: 19,
1978 },
1979 rendered: "abc",
1980 },
1981 source: Span {
1982 data: "abc",
1983 line: 3,
1984 col: 1,
1985 offset: 19,
1986 },
1987 style: SimpleBlockStyle::Paragraph,
1988 title_source: None,
1989 title: None,
1990 caption: None,
1991 number: None,
1992 anchor: None,
1993 anchor_reftext: None,
1994 attrlist: None,
1995 })],
1996 source: Span {
1997 data: "=== Section Title\n\nabc",
1998 line: 1,
1999 col: 1,
2000 offset: 0,
2001 },
2002 title_source: None,
2003 title: None,
2004 anchor: None,
2005 anchor_reftext: None,
2006 attrlist: None,
2007 section_type: SectionType::Normal,
2008 section_id: Some("_section_title"),
2009 caption: None,
2010 section_number: None,
2011 }
2012 );
2013
2014 assert_eq!(
2015 mi.after,
2016 Span {
2017 data: "== Section 2\n\ndef",
2018 line: 5,
2019 col: 1,
2020 offset: 24
2021 }
2022 );
2023 }
2024
2025 #[test]
2026 fn comment_transfer_boundary_respects_leveloffset() {
2027 let doc = Parser::default().parse(
2036 "= Doc\n:leveloffset: +2\n\n== Parent\n\npara\n\n[[x]]\n// comment\n= Child\n\nbody",
2037 );
2038
2039 let top: Vec<_> = doc.child_blocks().collect();
2040 assert_eq!(top.len(), 2, "Child must be a sibling of Parent");
2041
2042 let parent = top.first().unwrap();
2043 let child = top.last().unwrap();
2044 assert_eq!(parent.id(), Some("_parent"));
2045
2046 assert_eq!(child.id(), Some("x"));
2049 assert!(
2050 parent
2051 .child_blocks()
2052 .all(|b| b.raw_context().as_ref() != "section")
2053 );
2054 }
2055
2056 #[test]
2057 fn section_title_with_markup() {
2058 let mut parser = Parser::default();
2059 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2060
2061 let mi = crate::blocks::SectionBlock::parse(
2062 &BlockMetadata::new("== Section with *bold* text"),
2063 &mut parser,
2064 &mut warnings,
2065 )
2066 .unwrap();
2067
2068 assert_eq!(
2069 mi.item.section_title_source(),
2070 Span {
2071 data: "Section with *bold* text",
2072 line: 1,
2073 col: 4,
2074 offset: 3,
2075 }
2076 );
2077
2078 assert_eq!(
2079 mi.item.section_title(),
2080 "Section with <strong>bold</strong> text"
2081 );
2082
2083 assert_eq!(mi.item.section_type(), SectionType::Normal);
2084 assert_eq!(mi.item.id().unwrap(), "_section_with_bold_text");
2085 }
2086
2087 #[test]
2088 fn section_title_with_special_chars() {
2089 let mut parser = Parser::default();
2090 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2091
2092 let mi = crate::blocks::SectionBlock::parse(
2093 &BlockMetadata::new("== Section with <brackets> & ampersands"),
2094 &mut parser,
2095 &mut warnings,
2096 )
2097 .unwrap();
2098
2099 assert_eq!(
2100 mi.item.section_title_source(),
2101 Span {
2102 data: "Section with <brackets> & ampersands",
2103 line: 1,
2104 col: 4,
2105 offset: 3,
2106 }
2107 );
2108
2109 assert_eq!(
2110 mi.item.section_title(),
2111 "Section with <brackets> & ampersands"
2112 );
2113
2114 assert_eq!(mi.item.id().unwrap(), "_section_with_ampersands");
2115 }
2116
2117 #[test]
2118 fn err_level_0_section_heading() {
2119 let mut parser = Parser::default();
2120 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2121
2122 let result = crate::blocks::SectionBlock::parse(
2123 &BlockMetadata::new("= Document Title"),
2124 &mut parser,
2125 &mut warnings,
2126 );
2127
2128 assert!(result.is_none());
2129
2130 assert_eq!(
2131 warnings,
2132 vec![Warning {
2133 source: Span {
2134 data: "= Document Title",
2135 line: 1,
2136 col: 1,
2137 offset: 0,
2138 },
2139 warning: WarningType::Level0SectionHeadingNotSupported,
2140 }]
2141 );
2142 }
2143
2144 #[test]
2145 fn err_section_heading_level_exceeds_maximum() {
2146 let mut parser = Parser::default();
2147 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2148
2149 let result = crate::blocks::SectionBlock::parse(
2150 &BlockMetadata::new("======= Level 6 Section"),
2151 &mut parser,
2152 &mut warnings,
2153 );
2154
2155 assert!(result.is_none());
2156
2157 assert_eq!(
2158 warnings,
2159 vec![Warning {
2160 source: Span {
2161 data: "======= Level 6 Section",
2162 line: 1,
2163 col: 1,
2164 offset: 0,
2165 },
2166 warning: WarningType::SectionHeadingLevelExceedsMaximum(6),
2167 }]
2168 );
2169 }
2170
2171 #[test]
2172 fn valid_maximum_level_5_section() {
2173 let mut parser = Parser::default();
2174 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2175
2176 let mi = crate::blocks::SectionBlock::parse(
2177 &BlockMetadata::new("====== Level 5 Section"),
2178 &mut parser,
2179 &mut warnings,
2180 )
2181 .unwrap();
2182
2183 assert!(warnings.is_empty());
2184
2185 assert_eq!(mi.item.level(), 5);
2186 assert_eq!(mi.item.section_title(), "Level 5 Section");
2187 assert_eq!(mi.item.section_type(), SectionType::Normal);
2188 assert_eq!(mi.item.id().unwrap(), "_level_5_section");
2189 }
2190
2191 #[test]
2192 fn warn_section_level_skipped() {
2193 let mut parser = Parser::default();
2194 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2195
2196 let mi = crate::blocks::SectionBlock::parse(
2197 &BlockMetadata::new("== Level 1\n\n==== Level 3 (skipped level 2)"),
2198 &mut parser,
2199 &mut warnings,
2200 )
2201 .unwrap();
2202
2203 assert_eq!(mi.item.level(), 1);
2204 assert_eq!(mi.item.section_title(), "Level 1");
2205 assert_eq!(mi.item.section_type(), SectionType::Normal);
2206 assert_eq!(mi.item.child_blocks().count(), 1);
2207 assert_eq!(mi.item.id().unwrap(), "_level_1");
2208
2209 assert_eq!(
2210 warnings,
2211 vec![Warning {
2212 source: Span {
2213 data: "==== Level 3 (skipped level 2)",
2214 line: 3,
2215 col: 1,
2216 offset: 12,
2217 },
2218 warning: WarningType::SectionHeadingLevelSkipped(1, 3),
2219 }]
2220 );
2221 }
2222 }
2223
2224 mod markdown_style_headings {
2225 use std::ops::Deref;
2226
2227 use crate::{
2228 blocks::{ContentModel, MediaType, metadata::BlockMetadata, section::SectionType},
2229 tests::prelude::*,
2230 };
2231
2232 #[test]
2233 fn err_missing_space_before_title() {
2234 let mut parser = Parser::default();
2235 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2236
2237 assert!(
2238 crate::blocks::SectionBlock::parse(
2239 &BlockMetadata::new("#blah blah"),
2240 &mut parser,
2241 &mut warnings
2242 )
2243 .is_none()
2244 );
2245 }
2246
2247 #[test]
2248 fn simplest_section_block() {
2249 let mut parser = Parser::default();
2250 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2251
2252 let mi = crate::blocks::SectionBlock::parse(
2253 &BlockMetadata::new("## Section Title"),
2254 &mut parser,
2255 &mut warnings,
2256 )
2257 .unwrap();
2258
2259 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2260 assert_eq!(mi.item.raw_context().deref(), "section");
2261 assert_eq!(mi.item.resolved_context().deref(), "section");
2262 assert!(mi.item.declared_style().is_none());
2263 assert_eq!(mi.item.id().unwrap(), "_section_title");
2264 assert!(mi.item.roles().is_empty());
2265 assert!(mi.item.options().is_empty());
2266 assert!(mi.item.title_source().is_none());
2267 assert!(mi.item.title().is_none());
2268 assert!(mi.item.anchor().is_none());
2269 assert!(mi.item.anchor_reftext().is_none());
2270 assert!(mi.item.attrlist().is_none());
2271 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2272
2273 assert_eq!(
2274 mi.item,
2275 SectionBlock {
2276 level: 1,
2277 section_title: Content {
2278 original: Span {
2279 data: "Section Title",
2280 line: 1,
2281 col: 4,
2282 offset: 3,
2283 },
2284 rendered: "Section Title",
2285 },
2286 blocks: &[],
2287 source: Span {
2288 data: "## Section Title",
2289 line: 1,
2290 col: 1,
2291 offset: 0,
2292 },
2293 title_source: None,
2294 title: None,
2295 anchor: None,
2296 anchor_reftext: None,
2297 attrlist: None,
2298 section_type: SectionType::Normal,
2299 section_id: Some("_section_title"),
2300 caption: None,
2301 section_number: None,
2302 }
2303 );
2304
2305 assert_eq!(
2306 mi.after,
2307 Span {
2308 data: "",
2309 line: 1,
2310 col: 17,
2311 offset: 16
2312 }
2313 );
2314 }
2315
2316 #[test]
2317 fn has_child_block() {
2318 let mut parser = Parser::default();
2319 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2320
2321 let mi = crate::blocks::SectionBlock::parse(
2322 &BlockMetadata::new("## Section Title\n\nabc"),
2323 &mut parser,
2324 &mut warnings,
2325 )
2326 .unwrap();
2327
2328 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2329 assert_eq!(mi.item.raw_context().deref(), "section");
2330 assert_eq!(mi.item.resolved_context().deref(), "section");
2331 assert!(mi.item.declared_style().is_none());
2332 assert_eq!(mi.item.id().unwrap(), "_section_title");
2333 assert!(mi.item.roles().is_empty());
2334 assert!(mi.item.options().is_empty());
2335 assert!(mi.item.title_source().is_none());
2336 assert!(mi.item.title().is_none());
2337 assert!(mi.item.anchor().is_none());
2338 assert!(mi.item.anchor_reftext().is_none());
2339 assert!(mi.item.attrlist().is_none());
2340 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2341
2342 assert_eq!(
2343 mi.item,
2344 SectionBlock {
2345 level: 1,
2346 section_title: Content {
2347 original: Span {
2348 data: "Section Title",
2349 line: 1,
2350 col: 4,
2351 offset: 3,
2352 },
2353 rendered: "Section Title",
2354 },
2355 blocks: &[Block::Simple(SimpleBlock {
2356 content: Content {
2357 original: Span {
2358 data: "abc",
2359 line: 3,
2360 col: 1,
2361 offset: 18,
2362 },
2363 rendered: "abc",
2364 },
2365 source: Span {
2366 data: "abc",
2367 line: 3,
2368 col: 1,
2369 offset: 18,
2370 },
2371 style: SimpleBlockStyle::Paragraph,
2372 title_source: None,
2373 title: None,
2374 caption: None,
2375 number: None,
2376 anchor: None,
2377 anchor_reftext: None,
2378 attrlist: None,
2379 })],
2380 source: Span {
2381 data: "## Section Title\n\nabc",
2382 line: 1,
2383 col: 1,
2384 offset: 0,
2385 },
2386 title_source: None,
2387 title: None,
2388 anchor: None,
2389 anchor_reftext: None,
2390 attrlist: None,
2391 section_type: SectionType::Normal,
2392 section_id: Some("_section_title"),
2393 caption: None,
2394 section_number: None,
2395 }
2396 );
2397
2398 assert_eq!(
2399 mi.after,
2400 Span {
2401 data: "",
2402 line: 3,
2403 col: 4,
2404 offset: 21
2405 }
2406 );
2407 }
2408
2409 #[test]
2410 fn has_macro_block_with_extra_blank_line() {
2411 let mut parser = Parser::default();
2412 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2413
2414 let mi = crate::blocks::SectionBlock::parse(
2415 &BlockMetadata::new(
2416 "## Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]\n\n",
2417 ),
2418 &mut parser,
2419 &mut warnings,
2420 )
2421 .unwrap();
2422
2423 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2424 assert_eq!(mi.item.raw_context().deref(), "section");
2425 assert_eq!(mi.item.resolved_context().deref(), "section");
2426 assert!(mi.item.declared_style().is_none());
2427 assert_eq!(mi.item.id().unwrap(), "_section_title");
2428 assert!(mi.item.roles().is_empty());
2429 assert!(mi.item.options().is_empty());
2430 assert!(mi.item.title_source().is_none());
2431 assert!(mi.item.title().is_none());
2432 assert!(mi.item.anchor().is_none());
2433 assert!(mi.item.anchor_reftext().is_none());
2434 assert!(mi.item.attrlist().is_none());
2435 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2436
2437 assert_eq!(
2438 mi.item,
2439 SectionBlock {
2440 level: 1,
2441 section_title: Content {
2442 original: Span {
2443 data: "Section Title",
2444 line: 1,
2445 col: 4,
2446 offset: 3,
2447 },
2448 rendered: "Section Title",
2449 },
2450 blocks: &[Block::Media(MediaBlock {
2451 type_: MediaType::Image,
2452 target: Span {
2453 data: "bar",
2454 line: 3,
2455 col: 8,
2456 offset: 25,
2457 },
2458 macro_attrlist: Attrlist {
2459 attributes: &[
2460 ElementAttribute {
2461 name: Some("alt"),
2462 shorthand_items: &[],
2463 value: "Sunset"
2464 },
2465 ElementAttribute {
2466 name: Some("width"),
2467 shorthand_items: &[],
2468 value: "300"
2469 },
2470 ElementAttribute {
2471 name: Some("height"),
2472 shorthand_items: &[],
2473 value: "400"
2474 }
2475 ],
2476 anchor: None,
2477 source: Span {
2478 data: "alt=Sunset,width=300,height=400",
2479 line: 3,
2480 col: 12,
2481 offset: 29,
2482 }
2483 },
2484 source: Span {
2485 data: "image::bar[alt=Sunset,width=300,height=400]",
2486 line: 3,
2487 col: 1,
2488 offset: 18,
2489 },
2490 title_source: None,
2491 title: None,
2492 caption: None,
2493 number: None,
2494 anchor: None,
2495 anchor_reftext: None,
2496 attrlist: None,
2497 })],
2498 source: Span {
2499 data: "## Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]",
2500 line: 1,
2501 col: 1,
2502 offset: 0,
2503 },
2504 title_source: None,
2505 title: None,
2506 anchor: None,
2507 anchor_reftext: None,
2508 attrlist: None,
2509 section_type: SectionType::Normal,
2510 section_id: Some("_section_title"),
2511 caption: None,
2512 section_number: None,
2513 }
2514 );
2515
2516 assert_eq!(
2517 mi.after,
2518 Span {
2519 data: "",
2520 line: 5,
2521 col: 1,
2522 offset: 63
2523 }
2524 );
2525 }
2526
2527 #[test]
2528 fn has_child_block_with_errors() {
2529 let mut parser = Parser::default();
2530 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2531
2532 let mi = crate::blocks::SectionBlock::parse(
2533 &BlockMetadata::new(
2534 "## Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
2535 ),
2536 &mut parser,
2537 &mut warnings,
2538 )
2539 .unwrap();
2540
2541 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2542 assert_eq!(mi.item.raw_context().deref(), "section");
2543 assert_eq!(mi.item.resolved_context().deref(), "section");
2544 assert!(mi.item.declared_style().is_none());
2545 assert_eq!(mi.item.id().unwrap(), "_section_title");
2546 assert!(mi.item.roles().is_empty());
2547 assert!(mi.item.options().is_empty());
2548 assert!(mi.item.title_source().is_none());
2549 assert!(mi.item.title().is_none());
2550 assert!(mi.item.anchor().is_none());
2551 assert!(mi.item.anchor_reftext().is_none());
2552 assert!(mi.item.attrlist().is_none());
2553 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2554
2555 assert_eq!(
2556 mi.item,
2557 SectionBlock {
2558 level: 1,
2559 section_title: Content {
2560 original: Span {
2561 data: "Section Title",
2562 line: 1,
2563 col: 4,
2564 offset: 3,
2565 },
2566 rendered: "Section Title",
2567 },
2568 blocks: &[Block::Media(MediaBlock {
2569 type_: MediaType::Image,
2570 target: Span {
2571 data: "bar",
2572 line: 3,
2573 col: 8,
2574 offset: 25,
2575 },
2576 macro_attrlist: Attrlist {
2577 attributes: &[
2578 ElementAttribute {
2579 name: Some("alt"),
2580 shorthand_items: &[],
2581 value: "Sunset"
2582 },
2583 ElementAttribute {
2584 name: Some("width"),
2585 shorthand_items: &[],
2586 value: "300"
2587 },
2588 ElementAttribute {
2589 name: Some("height"),
2590 shorthand_items: &[],
2591 value: "400"
2592 }
2593 ],
2594 anchor: None,
2595 source: Span {
2596 data: "alt=Sunset,width=300,,height=400",
2597 line: 3,
2598 col: 12,
2599 offset: 29,
2600 }
2601 },
2602 source: Span {
2603 data: "image::bar[alt=Sunset,width=300,,height=400]",
2604 line: 3,
2605 col: 1,
2606 offset: 18,
2607 },
2608 title_source: None,
2609 title: None,
2610 caption: None,
2611 number: None,
2612 anchor: None,
2613 anchor_reftext: None,
2614 attrlist: None,
2615 })],
2616 source: Span {
2617 data: "## Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
2618 line: 1,
2619 col: 1,
2620 offset: 0,
2621 },
2622 title_source: None,
2623 title: None,
2624 anchor: None,
2625 anchor_reftext: None,
2626 attrlist: None,
2627 section_type: SectionType::Normal,
2628 section_id: Some("_section_title"),
2629 caption: None,
2630 section_number: None,
2631 }
2632 );
2633
2634 assert_eq!(
2635 mi.after,
2636 Span {
2637 data: "",
2638 line: 3,
2639 col: 45,
2640 offset: 62
2641 }
2642 );
2643
2644 assert_eq!(
2645 warnings,
2646 vec![Warning {
2647 source: Span {
2648 data: "alt=Sunset,width=300,,height=400",
2649 line: 3,
2650 col: 12,
2651 offset: 29,
2652 },
2653 warning: WarningType::EmptyAttributeValue,
2654 }]
2655 );
2656 }
2657
2658 #[test]
2659 fn dont_stop_at_child_section() {
2660 let mut parser = Parser::default();
2661 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2662
2663 let mi = crate::blocks::SectionBlock::parse(
2664 &BlockMetadata::new("## Section Title\n\nabc\n\n### Section 2\n\ndef"),
2665 &mut parser,
2666 &mut warnings,
2667 )
2668 .unwrap();
2669
2670 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2671 assert_eq!(mi.item.raw_context().deref(), "section");
2672 assert_eq!(mi.item.resolved_context().deref(), "section");
2673 assert!(mi.item.declared_style().is_none());
2674 assert_eq!(mi.item.id().unwrap(), "_section_title");
2675 assert!(mi.item.roles().is_empty());
2676 assert!(mi.item.options().is_empty());
2677 assert!(mi.item.title_source().is_none());
2678 assert!(mi.item.title().is_none());
2679 assert!(mi.item.anchor().is_none());
2680 assert!(mi.item.anchor_reftext().is_none());
2681 assert!(mi.item.attrlist().is_none());
2682 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2683
2684 assert_eq!(
2685 mi.item,
2686 SectionBlock {
2687 level: 1,
2688 section_title: Content {
2689 original: Span {
2690 data: "Section Title",
2691 line: 1,
2692 col: 4,
2693 offset: 3,
2694 },
2695 rendered: "Section Title",
2696 },
2697 blocks: &[
2698 Block::Simple(SimpleBlock {
2699 content: Content {
2700 original: Span {
2701 data: "abc",
2702 line: 3,
2703 col: 1,
2704 offset: 18,
2705 },
2706 rendered: "abc",
2707 },
2708 source: Span {
2709 data: "abc",
2710 line: 3,
2711 col: 1,
2712 offset: 18,
2713 },
2714 style: SimpleBlockStyle::Paragraph,
2715 title_source: None,
2716 title: None,
2717 caption: None,
2718 number: None,
2719 anchor: None,
2720 anchor_reftext: None,
2721 attrlist: None,
2722 }),
2723 Block::Section(SectionBlock {
2724 level: 2,
2725 section_title: Content {
2726 original: Span {
2727 data: "Section 2",
2728 line: 5,
2729 col: 5,
2730 offset: 27,
2731 },
2732 rendered: "Section 2",
2733 },
2734 blocks: &[Block::Simple(SimpleBlock {
2735 content: Content {
2736 original: Span {
2737 data: "def",
2738 line: 7,
2739 col: 1,
2740 offset: 38,
2741 },
2742 rendered: "def",
2743 },
2744 source: Span {
2745 data: "def",
2746 line: 7,
2747 col: 1,
2748 offset: 38,
2749 },
2750 style: SimpleBlockStyle::Paragraph,
2751 title_source: None,
2752 title: None,
2753 caption: None,
2754 number: None,
2755 anchor: None,
2756 anchor_reftext: None,
2757 attrlist: None,
2758 })],
2759 source: Span {
2760 data: "### Section 2\n\ndef",
2761 line: 5,
2762 col: 1,
2763 offset: 23,
2764 },
2765 title_source: None,
2766 title: None,
2767 anchor: None,
2768 anchor_reftext: None,
2769 attrlist: None,
2770 section_type: SectionType::Normal,
2771 section_id: Some("_section_2"),
2772 caption: None,
2773 section_number: None,
2774 })
2775 ],
2776 source: Span {
2777 data: "## Section Title\n\nabc\n\n### Section 2\n\ndef",
2778 line: 1,
2779 col: 1,
2780 offset: 0,
2781 },
2782 title_source: None,
2783 title: None,
2784 anchor: None,
2785 anchor_reftext: None,
2786 attrlist: None,
2787 section_type: SectionType::Normal,
2788 section_id: Some("_section_title"),
2789 caption: None,
2790 section_number: None,
2791 }
2792 );
2793
2794 assert_eq!(
2795 mi.after,
2796 Span {
2797 data: "",
2798 line: 7,
2799 col: 4,
2800 offset: 41
2801 }
2802 );
2803 }
2804
2805 #[test]
2806 fn stop_at_peer_section() {
2807 let mut parser = Parser::default();
2808 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2809
2810 let mi = crate::blocks::SectionBlock::parse(
2811 &BlockMetadata::new("## Section Title\n\nabc\n\n## Section 2\n\ndef"),
2812 &mut parser,
2813 &mut warnings,
2814 )
2815 .unwrap();
2816
2817 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2818 assert_eq!(mi.item.raw_context().deref(), "section");
2819 assert_eq!(mi.item.resolved_context().deref(), "section");
2820 assert!(mi.item.declared_style().is_none());
2821 assert_eq!(mi.item.id().unwrap(), "_section_title");
2822 assert!(mi.item.roles().is_empty());
2823 assert!(mi.item.options().is_empty());
2824 assert!(mi.item.title_source().is_none());
2825 assert!(mi.item.title().is_none());
2826 assert!(mi.item.anchor().is_none());
2827 assert!(mi.item.anchor_reftext().is_none());
2828 assert!(mi.item.attrlist().is_none());
2829 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2830
2831 assert_eq!(
2832 mi.item,
2833 SectionBlock {
2834 level: 1,
2835 section_title: Content {
2836 original: Span {
2837 data: "Section Title",
2838 line: 1,
2839 col: 4,
2840 offset: 3,
2841 },
2842 rendered: "Section Title",
2843 },
2844 blocks: &[Block::Simple(SimpleBlock {
2845 content: Content {
2846 original: Span {
2847 data: "abc",
2848 line: 3,
2849 col: 1,
2850 offset: 18,
2851 },
2852 rendered: "abc",
2853 },
2854 source: Span {
2855 data: "abc",
2856 line: 3,
2857 col: 1,
2858 offset: 18,
2859 },
2860 style: SimpleBlockStyle::Paragraph,
2861 title_source: None,
2862 title: None,
2863 caption: None,
2864 number: None,
2865 anchor: None,
2866 anchor_reftext: None,
2867 attrlist: None,
2868 })],
2869 source: Span {
2870 data: "## Section Title\n\nabc",
2871 line: 1,
2872 col: 1,
2873 offset: 0,
2874 },
2875 title_source: None,
2876 title: None,
2877 anchor: None,
2878 anchor_reftext: None,
2879 attrlist: None,
2880 section_type: SectionType::Normal,
2881 section_id: Some("_section_title"),
2882 caption: None,
2883 section_number: None,
2884 }
2885 );
2886
2887 assert_eq!(
2888 mi.after,
2889 Span {
2890 data: "## Section 2\n\ndef",
2891 line: 5,
2892 col: 1,
2893 offset: 23
2894 }
2895 );
2896 }
2897
2898 #[test]
2899 fn stop_at_ancestor_section() {
2900 let mut parser = Parser::default();
2901 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2902
2903 let mi = crate::blocks::SectionBlock::parse(
2904 &BlockMetadata::new("### Section Title\n\nabc\n\n## Section 2\n\ndef"),
2905 &mut parser,
2906 &mut warnings,
2907 )
2908 .unwrap();
2909
2910 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2911 assert_eq!(mi.item.raw_context().deref(), "section");
2912 assert_eq!(mi.item.resolved_context().deref(), "section");
2913 assert!(mi.item.declared_style().is_none());
2914 assert_eq!(mi.item.id().unwrap(), "_section_title");
2915 assert!(mi.item.roles().is_empty());
2916 assert!(mi.item.options().is_empty());
2917 assert!(mi.item.title_source().is_none());
2918 assert!(mi.item.title().is_none());
2919 assert!(mi.item.anchor().is_none());
2920 assert!(mi.item.anchor_reftext().is_none());
2921 assert!(mi.item.attrlist().is_none());
2922 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2923
2924 assert_eq!(
2925 mi.item,
2926 SectionBlock {
2927 level: 2,
2928 section_title: Content {
2929 original: Span {
2930 data: "Section Title",
2931 line: 1,
2932 col: 5,
2933 offset: 4,
2934 },
2935 rendered: "Section Title",
2936 },
2937 blocks: &[Block::Simple(SimpleBlock {
2938 content: Content {
2939 original: Span {
2940 data: "abc",
2941 line: 3,
2942 col: 1,
2943 offset: 19,
2944 },
2945 rendered: "abc",
2946 },
2947 source: Span {
2948 data: "abc",
2949 line: 3,
2950 col: 1,
2951 offset: 19,
2952 },
2953 style: SimpleBlockStyle::Paragraph,
2954 title_source: None,
2955 title: None,
2956 caption: None,
2957 number: None,
2958 anchor: None,
2959 anchor_reftext: None,
2960 attrlist: None,
2961 })],
2962 source: Span {
2963 data: "### Section Title\n\nabc",
2964 line: 1,
2965 col: 1,
2966 offset: 0,
2967 },
2968 title_source: None,
2969 title: None,
2970 anchor: None,
2971 anchor_reftext: None,
2972 attrlist: None,
2973 section_type: SectionType::Normal,
2974 section_id: Some("_section_title"),
2975 caption: None,
2976 section_number: None,
2977 }
2978 );
2979
2980 assert_eq!(
2981 mi.after,
2982 Span {
2983 data: "## Section 2\n\ndef",
2984 line: 5,
2985 col: 1,
2986 offset: 24
2987 }
2988 );
2989 }
2990
2991 #[test]
2992 fn section_title_with_markup() {
2993 let mut parser = Parser::default();
2994 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2995
2996 let mi = crate::blocks::SectionBlock::parse(
2997 &BlockMetadata::new("## Section with *bold* text"),
2998 &mut parser,
2999 &mut warnings,
3000 )
3001 .unwrap();
3002
3003 assert_eq!(
3004 mi.item.section_title_source(),
3005 Span {
3006 data: "Section with *bold* text",
3007 line: 1,
3008 col: 4,
3009 offset: 3,
3010 }
3011 );
3012
3013 assert_eq!(
3014 mi.item.section_title(),
3015 "Section with <strong>bold</strong> text"
3016 );
3017
3018 assert_eq!(mi.item.section_type(), SectionType::Normal);
3019 assert_eq!(mi.item.id().unwrap(), "_section_with_bold_text");
3020 }
3021
3022 #[test]
3023 fn section_title_with_special_chars() {
3024 let mut parser = Parser::default();
3025 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3026
3027 let mi = crate::blocks::SectionBlock::parse(
3028 &BlockMetadata::new("## Section with <brackets> & ampersands"),
3029 &mut parser,
3030 &mut warnings,
3031 )
3032 .unwrap();
3033
3034 assert_eq!(
3035 mi.item.section_title_source(),
3036 Span {
3037 data: "Section with <brackets> & ampersands",
3038 line: 1,
3039 col: 4,
3040 offset: 3,
3041 }
3042 );
3043
3044 assert_eq!(
3045 mi.item.section_title(),
3046 "Section with <brackets> & ampersands"
3047 );
3048
3049 assert_eq!(mi.item.section_type(), SectionType::Normal);
3050 }
3051
3052 #[test]
3053 fn err_level_0_section_heading() {
3054 let mut parser = Parser::default();
3055 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3056
3057 let result = crate::blocks::SectionBlock::parse(
3058 &BlockMetadata::new("# Document Title"),
3059 &mut parser,
3060 &mut warnings,
3061 );
3062
3063 assert!(result.is_none());
3064
3065 assert_eq!(
3066 warnings,
3067 vec![Warning {
3068 source: Span {
3069 data: "# Document Title",
3070 line: 1,
3071 col: 1,
3072 offset: 0,
3073 },
3074 warning: WarningType::Level0SectionHeadingNotSupported,
3075 }]
3076 );
3077 }
3078
3079 #[test]
3080 fn err_section_heading_level_exceeds_maximum() {
3081 let mut parser = Parser::default();
3082 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3083
3084 let result = crate::blocks::SectionBlock::parse(
3085 &BlockMetadata::new("####### Level 6 Section"),
3086 &mut parser,
3087 &mut warnings,
3088 );
3089
3090 assert!(result.is_none());
3091
3092 assert_eq!(
3093 warnings,
3094 vec![Warning {
3095 source: Span {
3096 data: "####### Level 6 Section",
3097 line: 1,
3098 col: 1,
3099 offset: 0,
3100 },
3101 warning: WarningType::SectionHeadingLevelExceedsMaximum(6),
3102 }]
3103 );
3104 }
3105
3106 #[test]
3107 fn valid_maximum_level_5_section() {
3108 let mut parser = Parser::default();
3109 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3110
3111 let mi = crate::blocks::SectionBlock::parse(
3112 &BlockMetadata::new("###### Level 5 Section"),
3113 &mut parser,
3114 &mut warnings,
3115 )
3116 .unwrap();
3117
3118 assert!(warnings.is_empty());
3119
3120 assert_eq!(mi.item.level(), 5);
3121 assert_eq!(mi.item.section_title(), "Level 5 Section");
3122 assert_eq!(mi.item.section_type(), SectionType::Normal);
3123 assert_eq!(mi.item.id().unwrap(), "_level_5_section");
3124 }
3125
3126 #[test]
3127 fn warn_section_level_skipped() {
3128 let mut parser = Parser::default();
3129 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3130
3131 let mi = crate::blocks::SectionBlock::parse(
3132 &BlockMetadata::new("## Level 1\n\n#### Level 3 (skipped level 2)"),
3133 &mut parser,
3134 &mut warnings,
3135 )
3136 .unwrap();
3137
3138 assert_eq!(mi.item.level(), 1);
3139 assert_eq!(mi.item.section_title(), "Level 1");
3140 assert_eq!(mi.item.section_type(), SectionType::Normal);
3141 assert_eq!(mi.item.child_blocks().count(), 1);
3142 assert_eq!(mi.item.id().unwrap(), "_level_1");
3143
3144 assert_eq!(
3145 warnings,
3146 vec![Warning {
3147 source: Span {
3148 data: "#### Level 3 (skipped level 2)",
3149 line: 3,
3150 col: 1,
3151 offset: 12,
3152 },
3153 warning: WarningType::SectionHeadingLevelSkipped(1, 3),
3154 }]
3155 );
3156 }
3157 }
3158
3159 #[test]
3160 fn warn_multiple_section_levels_skipped() {
3161 let mut parser = Parser::default();
3162 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3163
3164 let mi = crate::blocks::SectionBlock::parse(
3165 &BlockMetadata::new("== Level 1\n\n===== Level 4 (skipped levels 2 and 3)"),
3166 &mut parser,
3167 &mut warnings,
3168 )
3169 .unwrap();
3170
3171 assert_eq!(mi.item.level(), 1);
3172 assert_eq!(mi.item.section_title(), "Level 1");
3173 assert_eq!(mi.item.section_type(), SectionType::Normal);
3174 assert_eq!(mi.item.child_blocks().count(), 1);
3175 assert_eq!(mi.item.id().unwrap(), "_level_1");
3176
3177 assert_eq!(
3178 warnings,
3179 vec![Warning {
3180 source: Span {
3181 data: "===== Level 4 (skipped levels 2 and 3)",
3182 line: 3,
3183 col: 1,
3184 offset: 12,
3185 },
3186 warning: WarningType::SectionHeadingLevelSkipped(1, 4),
3187 }]
3188 );
3189 }
3190
3191 #[test]
3192 fn no_warning_for_consecutive_section_levels() {
3193 let mut parser = Parser::default();
3194 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3195
3196 let mi = crate::blocks::SectionBlock::parse(
3197 &BlockMetadata::new("== Level 1\n\n=== Level 2 (no skip)"),
3198 &mut parser,
3199 &mut warnings,
3200 )
3201 .unwrap();
3202
3203 assert_eq!(mi.item.level(), 1);
3204 assert_eq!(mi.item.section_title(), "Level 1");
3205 assert_eq!(mi.item.section_type(), SectionType::Normal);
3206 assert_eq!(mi.item.child_blocks().count(), 1);
3207 assert_eq!(mi.item.id().unwrap(), "_level_1");
3208
3209 assert!(warnings.is_empty());
3210 }
3211
3212 #[test]
3213 fn section_id_generation_basic() {
3214 let input = "== Section One";
3215 let mut parser = Parser::default();
3216 let document = parser.parse(input);
3217
3218 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3219 assert_eq!(section.id(), Some("_section_one"));
3220 } else {
3221 panic!("Expected section block");
3222 }
3223 }
3224
3225 #[test]
3226 fn section_id_generation_with_special_characters() {
3227 let input = "== We're back! & Company";
3228 let mut parser = Parser::default();
3229 let document = parser.parse(input);
3230
3231 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3232 assert_eq!(section.id(), Some("_were_back_company"));
3233 } else {
3234 panic!("Expected section block");
3235 }
3236 }
3237
3238 #[test]
3239 fn section_id_generation_with_entities() {
3240 let input = "== Ben & Jerry "Ice Cream"";
3241 let mut parser = Parser::default();
3242 let document = parser.parse(input);
3243
3244 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3245 assert_eq!(section.id(), Some("_ben_jerry_ice_cream"));
3246 } else {
3247 panic!("Expected section block");
3248 }
3249 }
3250
3251 #[test]
3252 fn section_id_generation_disabled_when_sectids_unset() {
3253 let input = ":!sectids:\n\n== Section One";
3254 let mut parser = Parser::default();
3255 let document = parser.parse(input);
3256
3257 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3258 assert_eq!(section.id(), None);
3259 } else {
3260 panic!("Expected section block");
3261 }
3262 }
3263
3264 #[test]
3265 fn section_id_generation_with_custom_prefix() {
3266 let input = ":idprefix: id_\n\n== Section One";
3267 let mut parser = Parser::default();
3268 let document = parser.parse(input);
3269
3270 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3271 assert_eq!(section.id(), Some("id_section_one"));
3272 } else {
3273 panic!("Expected section block");
3274 }
3275 }
3276
3277 #[test]
3278 fn section_id_generation_with_custom_separator() {
3279 let input = ":idseparator: -\n\n== Section One";
3280 let mut parser = Parser::default();
3281 let document = parser.parse(input);
3282
3283 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3284 assert_eq!(section.id(), Some("_section-one"));
3285 } else {
3286 panic!("Expected section block");
3287 }
3288 }
3289
3290 #[test]
3291 fn section_id_generation_with_empty_prefix() {
3292 let input = ":idprefix:\n\n== Section One";
3293 let mut parser = Parser::default();
3294 let document = parser.parse(input);
3295
3296 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3297 assert_eq!(section.id(), Some("section_one"));
3298 } else {
3299 panic!("Expected section block");
3300 }
3301 }
3302
3303 #[test]
3304 fn section_id_generation_removes_trailing_separator() {
3305 let input = ":idseparator: -\n\n== Section Title-";
3306 let mut parser = Parser::default();
3307 let document = parser.parse(input);
3308
3309 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3310 assert_eq!(section.id(), Some("_section-title"));
3311 } else {
3312 panic!("Expected section block");
3313 }
3314 }
3315
3316 #[test]
3317 fn section_id_generation_removes_leading_separator_when_prefix_empty() {
3318 let input = ":idprefix:\n:idseparator: -\n\n== -Section Title";
3319 let mut parser = Parser::default();
3320 let document = parser.parse(input);
3321
3322 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3323 assert_eq!(section.id(), Some("section-title"));
3324 } else {
3325 panic!("Expected section block");
3326 }
3327 }
3328
3329 #[test]
3330 fn section_id_generation_handles_multiple_trailing_separators() {
3331 let input = ":idseparator: _\n\n== Title with Multiple Dots...";
3332 let mut parser = Parser::default();
3333 let document = parser.parse(input);
3334
3335 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3336 assert_eq!(section.id(), Some("_title_with_multiple_dots"));
3337 } else {
3338 panic!("Expected section block");
3339 }
3340 }
3341
3342 #[test]
3343 fn warn_duplicate_manual_section_id() {
3344 let input = "[#my_id]\n== First Section\n\n[#my_id]\n== Second Section";
3345 let mut parser = Parser::default();
3346 let document = parser.parse(input);
3347
3348 let mut warnings = document.warnings();
3349
3350 assert_eq!(
3351 warnings.next().unwrap(),
3352 Warning {
3353 source: Span {
3354 data: "[#my_id]\n== Second Section",
3355 line: 4,
3356 col: 1,
3357 offset: 27,
3358 },
3359 warning: WarningType::DuplicateId("my_id".to_owned()),
3360 }
3361 );
3362
3363 assert!(warnings.next().is_none());
3364 }
3365
3366 #[test]
3367 fn section_with_custom_reftext_attribute() {
3368 let input = "[reftext=\"Custom Reference Text\"]\n== Section Title";
3369 let mut parser = Parser::default();
3370 let document = parser.parse(input);
3371
3372 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3373 assert_eq!(section.id(), Some("_section_title"));
3374 } else {
3375 panic!("Expected section block");
3376 }
3377
3378 let catalog = document.catalog();
3379 let entry = catalog.get_ref("_section_title");
3380 assert!(entry.is_some());
3381 assert_eq!(
3382 entry.unwrap().reftext,
3383 Some("Custom Reference Text".to_string())
3384 );
3385 }
3386
3387 #[test]
3388 fn section_without_reftext_uses_title() {
3389 let input = "== Section Title";
3390 let mut parser = Parser::default();
3391 let document = parser.parse(input);
3392
3393 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
3394 assert_eq!(section.id(), Some("_section_title"));
3395 } else {
3396 panic!("Expected section block");
3397 }
3398
3399 let catalog = document.catalog();
3400 let entry = catalog.get_ref("_section_title");
3401 assert!(entry.is_some());
3402 assert_eq!(entry.unwrap().reftext, Some("Section Title".to_string()));
3403 }
3404
3405 mod section_numbering {
3406 use crate::{blocks::Block, tests::prelude::*};
3407
3408 #[test]
3409 fn single_section_with_sectnums() {
3410 let input = ":sectnums:\n\n== First Section";
3411 let mut parser = Parser::default();
3412 let document = parser.parse(input);
3413
3414 if let Some(Block::Section(section)) = document.child_blocks().next() {
3415 let section_number = section.section_number();
3416 assert!(section_number.is_some());
3417 assert_eq!(section_number.unwrap().to_string(), "1");
3418 assert_eq!(section_number.unwrap().components(), [1]);
3419 } else {
3420 panic!("Expected section block");
3421 }
3422 }
3423
3424 #[test]
3425 fn multiple_level_1_sections() {
3426 let input = ":sectnums:\n\n== First Section\n\n== Second Section\n\n== Third Section";
3427 let mut parser = Parser::default();
3428 let document = parser.parse(input);
3429
3430 let mut sections = document.child_blocks().filter_map(|block| {
3431 if let Block::Section(section) = block {
3432 Some(section)
3433 } else {
3434 None
3435 }
3436 });
3437
3438 let first = sections.next().unwrap();
3439 assert_eq!(first.section_number().unwrap().to_string(), "1");
3440
3441 let second = sections.next().unwrap();
3442 assert_eq!(second.section_number().unwrap().to_string(), "2");
3443
3444 let third = sections.next().unwrap();
3445 assert_eq!(third.section_number().unwrap().to_string(), "3");
3446 }
3447
3448 #[test]
3449 fn nested_sections() {
3450 let input = ":sectnums:\n\n== Level 1\n\n=== Level 2\n\n==== Level 3";
3451 let document = Parser::default().parse(input);
3452
3453 if let Some(Block::Section(level1)) = document.child_blocks().next() {
3454 assert_eq!(level1.section_number().unwrap().to_string(), "1");
3455
3456 if let Some(Block::Section(level2)) = level1.child_blocks().next() {
3457 assert_eq!(level2.section_number().unwrap().to_string(), "1.1");
3458
3459 if let Some(Block::Section(level3)) = level2.child_blocks().next() {
3460 assert_eq!(level3.section_number().unwrap().to_string(), "1.1.1");
3461 } else {
3462 panic!("Expected level 3 section");
3463 }
3464 } else {
3465 panic!("Expected level 2 section");
3466 }
3467 } else {
3468 panic!("Expected level 1 section");
3469 }
3470 }
3471
3472 #[test]
3473 fn mixed_section_levels() {
3474 let input = ":sectnums:\n\n== First\n\n=== First.One\n\n=== First.Two\n\n== Second\n\n=== Second.One";
3475 let document = Parser::default().parse(input);
3476
3477 let mut sections = document.child_blocks().filter_map(|block| {
3478 if let Block::Section(section) = block {
3479 Some(section)
3480 } else {
3481 None
3482 }
3483 });
3484
3485 let first = sections.next().unwrap();
3486 assert_eq!(first.section_number().unwrap().to_string(), "1");
3487
3488 let first_one = first
3489 .child_blocks()
3490 .filter_map(|block| {
3491 if let Block::Section(section) = block {
3492 Some(section)
3493 } else {
3494 None
3495 }
3496 })
3497 .next()
3498 .unwrap();
3499 assert_eq!(first_one.section_number().unwrap().to_string(), "1.1");
3500
3501 let first_two = first
3502 .child_blocks()
3503 .filter_map(|block| {
3504 if let Block::Section(section) = block {
3505 Some(section)
3506 } else {
3507 None
3508 }
3509 })
3510 .nth(1)
3511 .unwrap();
3512 assert_eq!(first_two.section_number().unwrap().to_string(), "1.2");
3513
3514 let second = sections.next().unwrap();
3515 assert_eq!(second.section_number().unwrap().to_string(), "2");
3516
3517 let second_one = second
3518 .child_blocks()
3519 .filter_map(|block| {
3520 if let Block::Section(section) = block {
3521 Some(section)
3522 } else {
3523 None
3524 }
3525 })
3526 .next()
3527 .unwrap();
3528 assert_eq!(second_one.section_number().unwrap().to_string(), "2.1");
3529 }
3530
3531 #[test]
3532 fn sectnums_disabled() {
3533 let input = "== First Section\n\n== Second Section";
3534 let mut parser = Parser::default();
3535 let document = parser.parse(input);
3536
3537 for block in document.child_blocks() {
3538 if let Block::Section(section) = block {
3539 assert!(section.section_number().is_none());
3540 }
3541 }
3542 }
3543
3544 #[test]
3545 fn sectnums_explicitly_unset() {
3546 let input = ":!sectnums:\n\n== First Section\n\n== Second Section";
3547 let mut parser = Parser::default();
3548 let document = parser.parse(input);
3549
3550 for block in document.child_blocks() {
3551 if let Block::Section(section) = block {
3552 assert!(section.section_number().is_none());
3553 }
3554 }
3555 }
3556
3557 #[test]
3558 fn numbered_alias_enables_numbering() {
3559 let input = ":numbered:\n\n== First Section\n\n== Second Section";
3563 let mut parser = Parser::default();
3564 let document = parser.parse(input);
3565
3566 assert!(parser.is_attribute_set("sectnums"));
3567
3568 let mut sections = document.child_blocks().filter_map(|block| {
3569 if let Block::Section(section) = block {
3570 Some(section)
3571 } else {
3572 None
3573 }
3574 });
3575
3576 assert_eq!(
3577 sections
3578 .next()
3579 .unwrap()
3580 .section_number()
3581 .unwrap()
3582 .to_string(),
3583 "1"
3584 );
3585 assert_eq!(
3586 sections
3587 .next()
3588 .unwrap()
3589 .section_number()
3590 .unwrap()
3591 .to_string(),
3592 "2"
3593 );
3594 }
3595
3596 #[test]
3597 fn numbered_alias_can_be_toggled_off_within_document() {
3598 let input =
3601 ":numbered:\n\n== Numbered\n\n:numbered!:\n\n== Unnumbered\n\n== Also Unnumbered";
3602 let mut parser = Parser::default();
3603 let document = parser.parse(input);
3604
3605 let mut sections = document.child_blocks().filter_map(|block| {
3606 if let Block::Section(section) = block {
3607 Some(section)
3608 } else {
3609 None
3610 }
3611 });
3612
3613 assert_eq!(
3614 sections
3615 .next()
3616 .unwrap()
3617 .section_number()
3618 .unwrap()
3619 .to_string(),
3620 "1"
3621 );
3622 assert!(sections.next().unwrap().section_number().is_none());
3623 assert!(sections.next().unwrap().section_number().is_none());
3624 }
3625
3626 #[test]
3627 fn deep_nesting() {
3628 let input = ":sectnums:\n:sectnumlevels: 5\n\n== Level 1\n\n=== Level 2\n\n==== Level 3\n\n===== Level 4\n\n====== Level 5";
3629 let document = Parser::default().parse(input);
3630
3631 if let Some(Block::Section(l1)) = document.child_blocks().next() {
3632 assert_eq!(l1.section_number().unwrap().to_string(), "1");
3633
3634 if let Some(Block::Section(l2)) = l1.child_blocks().next() {
3635 assert_eq!(l2.section_number().unwrap().to_string(), "1.1");
3636
3637 if let Some(Block::Section(l3)) = l2.child_blocks().next() {
3638 assert_eq!(l3.section_number().unwrap().to_string(), "1.1.1");
3639
3640 if let Some(Block::Section(l4)) = l3.child_blocks().next() {
3641 assert_eq!(l4.section_number().unwrap().to_string(), "1.1.1.1");
3642
3643 if let Some(Block::Section(l5)) = l4.child_blocks().next() {
3644 assert_eq!(l5.section_number().unwrap().to_string(), "1.1.1.1.1");
3645 } else {
3646 panic!("Expected level 5 section");
3647 }
3648 } else {
3649 panic!("Expected level 4 section");
3650 }
3651 } else {
3652 panic!("Expected level 3 section");
3653 }
3654 } else {
3655 panic!("Expected level 2 section");
3656 }
3657 } else {
3658 panic!("Expected level 1 section");
3659 }
3660 }
3661 }
3662
3663 #[test]
3664 fn impl_debug() {
3665 let mut parser = Parser::default();
3666 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3667
3668 let section = crate::blocks::SectionBlock::parse(
3669 &BlockMetadata::new("== Section Title"),
3670 &mut parser,
3671 &mut warnings,
3672 )
3673 .unwrap()
3674 .item;
3675
3676 assert_eq!(
3677 format!("{section:#?}"),
3678 r#"SectionBlock {
3679 level: 1,
3680 section_title: Content {
3681 original: Span {
3682 data: "Section Title",
3683 line: 1,
3684 col: 4,
3685 offset: 3,
3686 },
3687 rendered: "Section Title",
3688 },
3689 blocks: &[],
3690 source: Span {
3691 data: "== Section Title",
3692 line: 1,
3693 col: 1,
3694 offset: 0,
3695 },
3696 title_source: None,
3697 title: None,
3698 anchor: None,
3699 anchor_reftext: None,
3700 attrlist: None,
3701 section_type: SectionType::Normal,
3702 section_id: Some(
3703 "_section_title",
3704 ),
3705 caption: None,
3706 section_number: None,
3707}"#
3708 );
3709 }
3710
3711 mod section_type {
3712 use crate::blocks::section::SectionType;
3713
3714 #[test]
3715 fn impl_debug() {
3716 let st = SectionType::Normal;
3717 assert_eq!(format!("{st:?}"), "SectionType::Normal");
3718
3719 let st = SectionType::Appendix;
3720 assert_eq!(format!("{st:?}"), "SectionType::Appendix");
3721
3722 let st = SectionType::Discrete;
3723 assert_eq!(format!("{st:?}"), "SectionType::Discrete");
3724 }
3725 }
3726
3727 mod section_number {
3728 mod assign_next_number {
3729 use crate::blocks::section::SectionNumber;
3730
3731 #[test]
3732 fn default() {
3733 let sn = SectionNumber::default();
3734 assert_eq!(sn.components(), []);
3735 assert_eq!(sn.to_string(), "");
3736 assert_eq!(
3737 format!("{sn:?}"),
3738 "SectionNumber { section_type: SectionType::Normal, components: &[], appendix_letter: None }"
3739 );
3740 }
3741
3742 #[test]
3743 fn level_1() {
3744 let mut sn = SectionNumber::default();
3745 sn.assign_next_number(1);
3746 assert_eq!(sn.components(), [1]);
3747 assert_eq!(sn.to_string(), "1");
3748 assert_eq!(
3749 format!("{sn:?}"),
3750 "SectionNumber { section_type: SectionType::Normal, components: &[1], appendix_letter: None }"
3751 );
3752 }
3753
3754 #[test]
3755 fn level_3() {
3756 let mut sn = SectionNumber::default();
3757 sn.assign_next_number(3);
3758 assert_eq!(sn.components(), [1, 1, 1]);
3759 assert_eq!(sn.to_string(), "1.1.1");
3760 assert_eq!(
3761 format!("{sn:?}"),
3762 "SectionNumber { section_type: SectionType::Normal, components: &[1, 1, 1], appendix_letter: None }"
3763 );
3764 }
3765
3766 #[test]
3767 fn level_3_then_1() {
3768 let mut sn = SectionNumber::default();
3769 sn.assign_next_number(3);
3770 sn.assign_next_number(1);
3771 assert_eq!(sn.components(), [2]);
3772 assert_eq!(sn.to_string(), "2");
3773 assert_eq!(
3774 format!("{sn:?}"),
3775 "SectionNumber { section_type: SectionType::Normal, components: &[2], appendix_letter: None }"
3776 );
3777 }
3778
3779 #[test]
3780 fn level_3_then_1_then_2() {
3781 let mut sn = SectionNumber::default();
3782 sn.assign_next_number(3);
3783 sn.assign_next_number(1);
3784 sn.assign_next_number(2);
3785 assert_eq!(sn.components(), [2, 1]);
3786 assert_eq!(sn.to_string(), "2.1");
3787 assert_eq!(
3788 format!("{sn:?}"),
3789 "SectionNumber { section_type: SectionType::Normal, components: &[2, 1], appendix_letter: None }"
3790 );
3791 }
3792 }
3793
3794 mod assign_next_number_appendix {
3795 use crate::blocks::{SectionType, section::SectionNumber};
3796
3797 #[test]
3798 fn default() {
3799 let sn = SectionNumber {
3800 section_type: SectionType::Appendix,
3801 components: vec![],
3802 appendix_letter: None,
3803 };
3804 assert_eq!(sn.components(), []);
3805 assert_eq!(sn.to_string(), "");
3806 assert_eq!(
3807 format!("{sn:?}"),
3808 "SectionNumber { section_type: SectionType::Appendix, components: &[], appendix_letter: None }"
3809 );
3810 }
3811
3812 #[test]
3813 fn level_1() {
3814 let mut sn = SectionNumber {
3815 section_type: SectionType::Appendix,
3816 components: vec![],
3817 appendix_letter: None,
3818 };
3819 sn.assign_next_number(1);
3820 assert_eq!(sn.components(), [1]);
3821 assert_eq!(sn.to_string(), "A");
3822 assert_eq!(
3823 format!("{sn:?}"),
3824 "SectionNumber { section_type: SectionType::Appendix, components: &[1], appendix_letter: None }"
3825 );
3826 }
3827
3828 #[test]
3829 fn level_3() {
3830 let mut sn = SectionNumber {
3831 section_type: SectionType::Appendix,
3832 components: vec![],
3833 appendix_letter: None,
3834 };
3835 sn.assign_next_number(3);
3836 assert_eq!(sn.components(), [1, 1, 1]);
3837 assert_eq!(sn.to_string(), "A.1.1");
3838 assert_eq!(
3839 format!("{sn:?}"),
3840 "SectionNumber { section_type: SectionType::Appendix, components: &[1, 1, 1], appendix_letter: None }"
3841 );
3842 }
3843
3844 #[test]
3845 fn level_3_then_1() {
3846 let mut sn = SectionNumber {
3847 section_type: SectionType::Appendix,
3848 components: vec![],
3849 appendix_letter: None,
3850 };
3851 sn.assign_next_number(3);
3852 sn.assign_next_number(1);
3853 assert_eq!(sn.components(), [2]);
3854 assert_eq!(sn.to_string(), "B");
3855 assert_eq!(
3856 format!("{sn:?}"),
3857 "SectionNumber { section_type: SectionType::Appendix, components: &[2], appendix_letter: None }"
3858 );
3859 }
3860
3861 #[test]
3862 fn level_3_then_1_then_2() {
3863 let mut sn = SectionNumber {
3864 section_type: SectionType::Appendix,
3865 components: vec![],
3866 appendix_letter: None,
3867 };
3868 sn.assign_next_number(3);
3869 sn.assign_next_number(1);
3870 sn.assign_next_number(2);
3871 assert_eq!(sn.components(), [2, 1]);
3872 assert_eq!(sn.to_string(), "B.1");
3873 assert_eq!(
3874 format!("{sn:?}"),
3875 "SectionNumber { section_type: SectionType::Appendix, components: &[2, 1], appendix_letter: None }"
3876 );
3877 }
3878
3879 #[test]
3880 fn appendix_letter_overrides_first_component() {
3881 let mut sn = SectionNumber {
3882 section_type: SectionType::Appendix,
3883 components: vec![],
3884 appendix_letter: Some("\u{3b2}".to_owned()),
3885 };
3886 sn.assign_next_number(1);
3887 sn.assign_next_number(2);
3888 assert_eq!(sn.components(), [1, 1]);
3889 assert_eq!(sn.appendix_letter(), Some("\u{3b2}"));
3890 assert_eq!(sn.to_string(), "\u{3b2}.1");
3891 assert_eq!(
3892 format!("{sn:?}"),
3893 "SectionNumber { section_type: SectionType::Appendix, components: &[1, 1], appendix_letter: Some(\"\u{3b2}\") }"
3894 );
3895 }
3896 }
3897 }
3898
3899 mod appendix_number_attribute {
3900 use crate::{blocks::Block, tests::prelude::*};
3901
3902 #[test]
3907 fn seeds_lettering_from_the_attribute() {
3908 let doc = Parser::default()
3909 .parse(":appendix-number: M\n\n[appendix]\n== One\n\n[appendix]\n== Two\n");
3910
3911 let caps: Vec<Option<&str>> = all_sections(&doc).iter().map(|s| s.caption()).collect();
3912 assert_eq!(caps, vec![Some("Appendix N: "), Some("Appendix O: ")]);
3913 }
3914
3915 #[test]
3916 fn increments_a_numeric_value_numerically() {
3917 let doc = Parser::default()
3918 .parse(":appendix-number: 9\n\n[appendix]\n== One\n\n[appendix]\n== Two\n");
3919
3920 let caps: Vec<Option<&str>> = all_sections(&doc).iter().map(|s| s.caption()).collect();
3921 assert_eq!(caps, vec![Some("Appendix 10: "), Some("Appendix 11: ")]);
3922 }
3923
3924 #[test]
3925 fn bare_attribute_resolves_to_default_seed() {
3926 let doc = Parser::default()
3929 .parse(":appendix-number:\n\n[appendix]\n== One\n\n[appendix]\n== Two\n");
3930
3931 let caps: Vec<Option<&str>> = all_sections(&doc).iter().map(|s| s.caption()).collect();
3932 assert_eq!(caps, vec![Some("Appendix A: "), Some("Appendix B: ")]);
3933 }
3934
3935 #[test]
3936 fn letters_section_numbers_of_appendix_and_subsections() {
3937 let doc = Parser::default()
3938 .parse(":sectnums:\n:appendix-number: \u{3b1}\n\n[appendix]\n== One\n\n=== Sub\n");
3939
3940 let nums: Vec<Option<String>> = all_sections(&doc)
3941 .iter()
3942 .map(|s| s.section_number().map(|n| n.to_string()))
3943 .collect();
3944 assert_eq!(
3945 nums,
3946 vec![Some("\u{3b2}".to_owned()), Some("\u{3b2}.1".to_owned())]
3947 );
3948 }
3949
3950 #[test]
3951 fn attribute_reads_back_as_the_current_letter() {
3952 let doc = Parser::default().parse("[appendix]\n== One\n\nLetter {appendix-number}.\n");
3955
3956 let section = first_section(&doc);
3957 let Some(Block::Simple(paragraph)) = section.child_blocks().next() else {
3958 panic!("expected a simple block");
3959 };
3960 assert_eq!(paragraph.content().rendered(), "Letter A.");
3961 }
3962 }
3963
3964 mod discrete_headings {
3965 use std::ops::Deref;
3966
3967 use crate::{
3968 blocks::{ContentModel, metadata::BlockMetadata, section::SectionType},
3969 tests::prelude::*,
3970 };
3971
3972 #[test]
3973 fn basic_case() {
3974 let mut parser = Parser::default();
3975 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3976
3977 let mi = crate::blocks::SectionBlock::parse(
3978 &BlockMetadata::new("[discrete]\n== Discrete Heading"),
3979 &mut parser,
3980 &mut warnings,
3981 )
3982 .unwrap();
3983
3984 assert_eq!(mi.item.content_model(), ContentModel::Compound);
3985 assert_eq!(mi.item.raw_context().deref(), "floating_title");
3986 assert_eq!(mi.item.resolved_context().deref(), "floating_title");
3987 assert_eq!(mi.item.level(), 1);
3988 assert_eq!(mi.item.section_title(), "Discrete Heading");
3989 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3990 assert!(mi.item.child_blocks().next().is_none());
3991 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
3992 assert!(mi.item.title().is_none());
3993 assert!(mi.item.anchor().is_none());
3994 assert!(mi.item.attrlist().is_some());
3995 assert_eq!(mi.item.section_number(), None);
3996 assert!(warnings.is_empty());
3997
3998 assert_eq!(
3999 mi.item.section_title_source(),
4000 Span {
4001 data: "Discrete Heading",
4002 line: 2,
4003 col: 4,
4004 offset: 14,
4005 }
4006 );
4007
4008 assert_eq!(
4009 mi.item.span(),
4010 Span {
4011 data: "[discrete]\n== Discrete Heading",
4012 line: 1,
4013 col: 1,
4014 offset: 0,
4015 }
4016 );
4017
4018 assert_eq!(
4019 mi.after,
4020 Span {
4021 data: "",
4022 line: 2,
4023 col: 20,
4024 offset: 30,
4025 }
4026 );
4027 }
4028
4029 #[test]
4030 fn float_style() {
4031 let mut parser = Parser::default();
4032 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4033
4034 let mi = crate::blocks::SectionBlock::parse(
4035 &BlockMetadata::new("[float]\n== Floating Heading"),
4036 &mut parser,
4037 &mut warnings,
4038 )
4039 .unwrap();
4040
4041 assert_eq!(mi.item.level(), 1);
4042 assert_eq!(mi.item.section_title(), "Floating Heading");
4043 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4044 assert!(mi.item.child_blocks().next().is_none());
4045 assert!(warnings.is_empty());
4046 }
4047
4048 #[test]
4049 fn has_no_child_blocks() {
4050 let mut parser = Parser::default();
4051 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4052
4053 let mi = crate::blocks::SectionBlock::parse(
4054 &BlockMetadata::new("[discrete]\n== Discrete Heading\n\nThis is a paragraph."),
4055 &mut parser,
4056 &mut warnings,
4057 )
4058 .unwrap();
4059
4060 assert_eq!(mi.item.level(), 1);
4061 assert_eq!(mi.item.section_title(), "Discrete Heading");
4062 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4063
4064 assert!(mi.item.child_blocks().next().is_none());
4066
4067 assert_eq!(
4069 mi.after,
4070 Span {
4071 data: "This is a paragraph.",
4072 line: 4,
4073 col: 1,
4074 offset: 32,
4075 }
4076 );
4077
4078 assert!(warnings.is_empty());
4079 }
4080
4081 #[test]
4082 fn not_in_section_hierarchy() {
4083 let input = "== Section 1\n\n[discrete]\n=== Discrete\n\n=== Section 1.1";
4084 let mut parser = Parser::default();
4085 let document = parser.parse(input);
4086
4087 let mut blocks = document.child_blocks();
4088
4089 if let Some(crate::blocks::Block::Section(section)) = blocks.next() {
4091 assert_eq!(section.section_title(), "Section 1");
4092 assert_eq!(section.level(), 1);
4093 assert_eq!(section.section_type(), SectionType::Normal);
4094
4095 let mut children = section.child_blocks();
4096
4097 if let Some(crate::blocks::Block::Section(discrete)) = children.next() {
4099 assert_eq!(discrete.section_title(), "Discrete");
4100 assert_eq!(discrete.level(), 2);
4101 assert_eq!(discrete.section_type(), SectionType::Discrete);
4102 assert!(discrete.child_blocks().next().is_none());
4103 } else {
4104 panic!("Expected discrete heading block");
4105 }
4106
4107 if let Some(crate::blocks::Block::Section(subsection)) = children.next() {
4109 assert_eq!(subsection.section_title(), "Section 1.1");
4110 assert_eq!(subsection.level(), 2);
4111 assert_eq!(subsection.section_type(), SectionType::Normal);
4112 } else {
4113 panic!("Expected subsection block");
4114 }
4115 } else {
4116 panic!("Expected section block");
4117 }
4118 }
4119
4120 #[test]
4121 fn has_auto_id() {
4122 let input = "[discrete]\n== Discrete Heading";
4123 let mut parser = Parser::default();
4124 let document = parser.parse(input);
4125
4126 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
4127 assert_eq!(section.id(), Some("_discrete_heading"));
4129 } else {
4130 panic!("Expected section block");
4131 }
4132 }
4133
4134 #[test]
4135 fn with_manual_id() {
4136 let input = "[discrete#my-id]\n== Discrete Heading";
4137 let mut parser = Parser::default();
4138 let document = parser.parse(input);
4139
4140 if let Some(crate::blocks::Block::Section(section)) = document.child_blocks().next() {
4141 assert_eq!(section.id(), Some("my-id"));
4143 } else {
4144 panic!("Expected section block");
4145 }
4146 }
4147
4148 #[test]
4149 fn no_section_number() {
4150 let input = ":sectnums:\n\n== Section 1\n\n[discrete]\n=== Discrete\n\n=== Section 1.1";
4151 let mut parser = Parser::default();
4152 let document = parser.parse(input);
4153
4154 let mut blocks = document.child_blocks();
4155
4156 if let Some(crate::blocks::Block::Section(section)) = blocks.next() {
4157 assert_eq!(section.section_title(), "Section 1");
4158 assert!(section.section_number().is_some());
4159
4160 let mut children = section.child_blocks();
4161
4162 if let Some(crate::blocks::Block::Section(discrete)) = children.next() {
4164 assert_eq!(discrete.section_title(), "Discrete");
4165 assert_eq!(discrete.section_number(), None);
4166 } else {
4167 panic!("Expected discrete heading block");
4168 }
4169
4170 if let Some(crate::blocks::Block::Section(subsection)) = children.next() {
4172 assert_eq!(subsection.section_title(), "Section 1.1");
4173 assert!(subsection.section_number().is_some());
4174 } else {
4175 panic!("Expected subsection block");
4176 }
4177 } else {
4178 panic!("Expected section block");
4179 }
4180 }
4181
4182 #[test]
4183 fn title_can_have_markup() {
4184 let mut parser = Parser::default();
4185 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4186
4187 let mi = crate::blocks::SectionBlock::parse(
4188 &BlockMetadata::new("[discrete]\n== Discrete with *bold* text"),
4189 &mut parser,
4190 &mut warnings,
4191 )
4192 .unwrap();
4193
4194 assert_eq!(
4195 mi.item.section_title(),
4196 "Discrete with <strong>bold</strong> text"
4197 );
4198 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4199 assert!(warnings.is_empty());
4200 }
4201
4202 #[test]
4203 fn level_0() {
4204 let mut parser = Parser::default();
4208 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4209
4210 let mi = crate::blocks::SectionBlock::parse(
4211 &BlockMetadata::new("[discrete]\n= Level 0 Discrete"),
4212 &mut parser,
4213 &mut warnings,
4214 )
4215 .unwrap();
4216
4217 assert_eq!(mi.item.level(), 0);
4218 assert_eq!(mi.item.section_title(), "Level 0 Discrete");
4219 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4220 assert!(warnings.is_empty());
4221 }
4222
4223 #[test]
4224 fn level_2() {
4225 let mut parser = Parser::default();
4226 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4227
4228 let mi = crate::blocks::SectionBlock::parse(
4229 &BlockMetadata::new("[discrete]\n=== Level 2 Discrete"),
4230 &mut parser,
4231 &mut warnings,
4232 )
4233 .unwrap();
4234
4235 assert_eq!(mi.item.level(), 2);
4236 assert_eq!(mi.item.section_title(), "Level 2 Discrete");
4237 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4238 assert!(warnings.is_empty());
4239 }
4240
4241 #[test]
4242 fn level_5() {
4243 let mut parser = Parser::default();
4244 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4245
4246 let mi = crate::blocks::SectionBlock::parse(
4247 &BlockMetadata::new("[discrete]\n====== Level 5 Discrete"),
4248 &mut parser,
4249 &mut warnings,
4250 )
4251 .unwrap();
4252
4253 assert_eq!(mi.item.level(), 5);
4254 assert_eq!(mi.item.section_title(), "Level 5 Discrete");
4255 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4256 assert!(warnings.is_empty());
4257 }
4258
4259 #[test]
4260 fn markdown_style() {
4261 let mut parser = Parser::default();
4262 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4263
4264 let mi = crate::blocks::SectionBlock::parse(
4265 &BlockMetadata::new("[discrete]\n## Discrete Heading"),
4266 &mut parser,
4267 &mut warnings,
4268 )
4269 .unwrap();
4270
4271 assert_eq!(mi.item.level(), 1);
4272 assert_eq!(mi.item.section_title(), "Discrete Heading");
4273 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4274 assert!(warnings.is_empty());
4275 }
4276
4277 #[test]
4278 fn with_block_title() {
4279 let mut parser = Parser::default();
4280 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4281
4282 let mi = crate::blocks::SectionBlock::parse(
4283 &BlockMetadata::new(".Block Title\n[discrete]\n== Discrete Heading"),
4284 &mut parser,
4285 &mut warnings,
4286 )
4287 .unwrap();
4288
4289 assert_eq!(mi.item.level(), 1);
4290 assert_eq!(mi.item.section_title(), "Discrete Heading");
4291 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4292 assert_eq!(mi.item.title(), Some("Block Title"));
4293 assert!(warnings.is_empty());
4294 }
4295
4296 #[test]
4297 fn with_anchor() {
4298 let mut parser = Parser::default();
4299 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4300
4301 let mi = crate::blocks::SectionBlock::parse(
4302 &BlockMetadata::new("[[my_anchor]]\n[discrete]\n== Discrete Heading"),
4303 &mut parser,
4304 &mut warnings,
4305 )
4306 .unwrap();
4307
4308 assert_eq!(mi.item.level(), 1);
4309 assert_eq!(mi.item.section_title(), "Discrete Heading");
4310 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4311 assert_eq!(mi.item.id(), Some("my_anchor"));
4312 assert!(warnings.is_empty());
4313 }
4314
4315 #[test]
4316 fn doesnt_include_subsequent_blocks() {
4317 let mut parser = Parser::default();
4318 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
4319
4320 let mi = crate::blocks::SectionBlock::parse(
4321 &BlockMetadata::new(
4322 "[discrete]\n== Discrete Heading\n\nparagraph\n\n== Next Section",
4323 ),
4324 &mut parser,
4325 &mut warnings,
4326 )
4327 .unwrap();
4328
4329 assert_eq!(mi.item.level(), 1);
4330 assert_eq!(mi.item.section_title(), "Discrete Heading");
4331 assert_eq!(mi.item.section_type(), SectionType::Discrete);
4332
4333 assert!(mi.item.child_blocks().next().is_none());
4335
4336 assert!(mi.after.data().contains("paragraph"));
4338 assert!(mi.after.data().contains("== Next Section"));
4339
4340 assert!(warnings.is_empty());
4341 }
4342 }
4343}