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