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