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