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