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