1use std::{fmt, slice::Iter, sync::LazyLock};
2
3use regex::Regex;
4
5use crate::{
6 HasSpan, Parser, Span,
7 attributes::Attrlist,
8 blocks::{
9 Block, ContentModel, IsBlock, metadata::BlockMetadata, parse_utils::parse_blocks_until,
10 },
11 content::{Content, SubstitutionGroup, strip_footnote_marker_spans},
12 document::{InterpretedValue, RefType},
13 internal::debug::DebugSliceReference,
14 parser::XrefSignifier,
15 span::MatchedItem,
16 strings::CowStr,
17 warnings::{Warning, WarningType},
18};
19
20#[derive(Clone, Eq, PartialEq)]
25pub struct SectionBlock<'src> {
26 level: usize,
27 section_title: Content<'src>,
28 blocks: Vec<Block<'src>>,
29 source: Span<'src>,
30 title_source: Option<Span<'src>>,
31 title: Option<String>,
32 anchor: Option<Span<'src>>,
33 anchor_reftext: Option<Span<'src>>,
34 attrlist: Option<Attrlist<'src>>,
35 section_type: SectionType,
36 section_id: Option<String>,
37 caption: Option<String>,
38 section_number: Option<SectionNumber>,
39}
40
41impl<'src> SectionBlock<'src> {
42 pub(crate) fn parse(
43 metadata: &BlockMetadata<'src>,
44 parser: &mut Parser,
45 warnings: &mut Vec<Warning<'src>>,
46 ) -> Option<MatchedItem<'src, Self>> {
47 let discrete = metadata.is_discrete();
48
49 let source = metadata.block_start.discard_empty_lines();
50
51 let level_and_title = parse_title_line(source, parser.level_offset(), warnings)?;
59
60 let sectids = parser.is_attribute_set("sectids");
63
64 let level = level_and_title.item.0;
65
66 let section_type = if discrete {
69 SectionType::Discrete
70 } else if level == 1 {
71 let section_type = if let Some(ref attrlist) = metadata.attrlist
72 && let Some(block_style) = attrlist.block_style()
73 && block_style == "appendix"
74 {
75 SectionType::Appendix
76 } else {
77 SectionType::Normal
78 };
79 parser.topmost_section_type = section_type;
80 section_type
81 } else {
82 parser.topmost_section_type
83 };
84
85 let sectnums_active =
95 parser.is_attribute_set("sectnums") && level <= parser.sectnumlevels && !discrete;
96
97 let is_appendix_root = !discrete && level == 1 && section_type == SectionType::Appendix;
98
99 let has_explicit_reftext = metadata
105 .attrlist
106 .as_ref()
107 .and_then(|a| a.named_attribute("reftext"))
108 .is_some()
109 || metadata.anchor_reftext.is_some();
110
111 let (section_number, caption, xref_signifier) = if is_appendix_root {
112 parser
113 .last_appendix_section_number
114 .assign_next_number(level);
115 let number = parser.last_appendix_section_number.clone();
116 let caption = appendix_caption(parser, &number);
117
118 let signifier = (!has_explicit_reftext).then(|| XrefSignifier {
122 label: join_signifier(
123 parser.attribute_value("appendix-refsig").as_maybe_str(),
124 &number.to_string(),
125 ),
126 emphasize: true,
127 });
128 let section_number = if sectnums_active { Some(number) } else { None };
129 (section_number, Some(caption), signifier)
130 } else if sectnums_active {
131 let number = parser.assign_section_number(level);
132 let signifier = (!has_explicit_reftext).then(|| XrefSignifier {
133 label: join_signifier(
134 parser.attribute_value("section-refsig").as_maybe_str(),
135 &number.to_string(),
136 ),
137 emphasize: false,
138 });
139 (Some(number), None, signifier)
140 } else {
141 (None, None, None)
142 };
143
144 let mut most_recent_level = level;
145
146 let mut section_title = Content::from(level_and_title.item.1);
165 parser.mark_footnote_spans.set(true);
166 SubstitutionGroup::Title.apply(&mut section_title, parser, metadata.attrlist.as_ref());
167 parser.mark_footnote_spans.set(false);
168
169 let title_reftext = strip_footnote_marker_spans(section_title.rendered());
172
173 section_title.remove_footnote_marker_sentinels();
176
177 let is_bibliography_section = !discrete
184 && metadata
185 .attrlist
186 .as_ref()
187 .and_then(|attrlist| attrlist.block_style())
188 == Some("bibliography");
189
190 let previously_in_bibliography_section = parser.parsing_bibliography_section_body;
191 parser.parsing_bibliography_section_body = is_bibliography_section;
192
193 let mut maw_blocks = parse_blocks_until(
194 level_and_title.after,
195 |i, parser| {
196 discrete
197 || peer_or_ancestor_section(*i, level, &mut most_recent_level, warnings, parser)
198 },
199 parser,
200 );
201
202 parser.parsing_bibliography_section_body = previously_in_bibliography_section;
203
204 let blocks = maw_blocks.item;
205 let source = metadata.source.trim_remainder(blocks.after);
206
207 let proposed_base_id = generate_section_id(&title_reftext, parser);
208
209 let manual_id = metadata
210 .attrlist
211 .as_ref()
212 .and_then(|a| a.id())
213 .or_else(|| metadata.anchor.as_ref().map(|anchor| anchor.data()));
214
215 let reftext = metadata
219 .attrlist
220 .as_ref()
221 .and_then(|a| a.named_attribute("reftext").map(|a| a.value()))
222 .or_else(|| metadata.anchor_reftext.as_ref().map(|span| span.data()))
223 .unwrap_or(&title_reftext);
224
225 let section_id = if sectids && manual_id.is_none() {
226 let id = parser.generate_and_register_unique_id(
227 &proposed_base_id,
228 Some(reftext),
229 RefType::Section,
230 );
231 if let Some(signifier) = xref_signifier {
232 parser.set_ref_signifier(&id, signifier);
233 }
234 Some(id)
235 } else {
236 if let Some(manual_id) = manual_id {
237 match parser.register_ref(manual_id, Some(reftext), RefType::Section) {
238 Ok(()) => {
239 if let Some(signifier) = xref_signifier {
240 parser.set_ref_signifier(manual_id, signifier);
241 }
242 }
243 Err(_duplicate_error) => {
244 warnings.push(Warning {
245 source: metadata.source.trim_remainder(level_and_title.after),
246 warning: WarningType::DuplicateId(manual_id.to_string()),
247 origin: None,
248 });
249 }
250 }
251 }
252
253 None
254 };
255
256 if level == 1 && !discrete {
258 parser.topmost_section_type = SectionType::Normal;
259 }
260
261 warnings.append(&mut maw_blocks.warnings);
262
263 Some(MatchedItem {
264 item: Self {
265 level,
266 section_title,
267 blocks: blocks.item,
268 source: source.trim_trailing_whitespace(),
269 title_source: metadata.title_source,
270 title: metadata.title.clone(),
271 anchor: metadata.anchor,
272 anchor_reftext: metadata.anchor_reftext,
273 attrlist: metadata.attrlist.clone(),
274 section_type,
275 section_id,
276 caption,
277 section_number,
278 },
279 after: blocks.after,
280 })
281 }
282
283 pub fn level(&self) -> usize {
293 self.level
294 }
295
296 pub fn section_title_source(&self) -> Span<'src> {
298 self.section_title.original()
299 }
300
301 pub fn section_title(&'src self) -> &'src str {
304 self.section_title.rendered()
305 }
306
307 pub fn section_type(&'src self) -> SectionType {
309 self.section_type
310 }
311
312 #[cfg(test)]
316 pub(crate) fn section_id(&'src self) -> Option<&'src str> {
317 self.section_id.as_deref()
318 }
319
320 pub fn section_number(&'src self) -> Option<&'src SectionNumber> {
322 self.section_number.as_ref()
323 }
324}
325
326fn appendix_caption(parser: &Parser, number: &SectionNumber) -> String {
334 let letter = number.to_string();
335 match parser.attribute_value("appendix-caption") {
336 InterpretedValue::Value(label) if !label.is_empty() => format!("{label} {letter}: "),
337 _ => format!("{letter}. "),
338 }
339}
340
341fn join_signifier(signifier: Option<&str>, number: &str) -> String {
347 match signifier {
348 Some(signifier) if !signifier.is_empty() => format!("{signifier} {number}"),
349 _ => number.to_string(),
350 }
351}
352
353impl<'src> IsBlock<'src> for SectionBlock<'src> {
354 fn content_model(&self) -> ContentModel {
355 ContentModel::Compound
356 }
357
358 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
359 Some(&mut self.section_title)
361 }
362
363 fn raw_context(&self) -> CowStr<'src> {
364 "section".into()
365 }
366
367 fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
368 &mut self.blocks
369 }
370
371 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
372 self.blocks.iter()
373 }
374
375 fn title_source(&'src self) -> Option<Span<'src>> {
376 self.title_source
377 }
378
379 fn title(&self) -> Option<&str> {
380 self.title.as_deref()
381 }
382
383 fn anchor(&'src self) -> Option<Span<'src>> {
384 self.anchor
385 }
386
387 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
388 self.anchor_reftext
389 }
390
391 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
392 self.attrlist.as_ref()
393 }
394
395 fn caption(&self) -> Option<&str> {
396 self.caption.as_deref()
397 }
398
399 fn id(&'src self) -> Option<&'src str> {
400 self.anchor()
402 .map(|a| a.data())
403 .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id()))
404 .or(self.section_id.as_deref())
406 }
407}
408
409impl<'src> HasSpan<'src> for SectionBlock<'src> {
410 fn span(&self) -> Span<'src> {
411 self.source
412 }
413}
414
415impl std::fmt::Debug for SectionBlock<'_> {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 f.debug_struct("SectionBlock")
418 .field("level", &self.level)
419 .field("section_title", &self.section_title)
420 .field("blocks", &DebugSliceReference(&self.blocks))
421 .field("source", &self.source)
422 .field("title_source", &self.title_source)
423 .field("title", &self.title)
424 .field("anchor", &self.anchor)
425 .field("anchor_reftext", &self.anchor_reftext)
426 .field("attrlist", &self.attrlist)
427 .field("section_type", &self.section_type)
428 .field("section_id", &self.section_id)
429 .field("caption", &self.caption)
430 .field("section_number", &self.section_number)
431 .finish()
432 }
433}
434
435const MIN_SECTION_LEVEL: i32 = 1;
439const MAX_SECTION_LEVEL: i32 = 5;
440
441fn parse_title_line<'src>(
456 source: Span<'src>,
457 offset: i32,
458 warnings: &mut Vec<Warning<'src>>,
459) -> Option<MatchedItem<'src, (usize, Span<'src>)>> {
460 let mi = source.take_non_empty_line()?;
461 let mut line = mi.item;
462
463 let mut count = 0;
464
465 if line.starts_with('=') {
466 while let Some(mi) = line.take_prefix("=") {
467 count += 1;
468 line = mi.after;
469 }
470 } else {
471 while let Some(mi) = line.take_prefix("#") {
472 count += 1;
473 line = mi.after;
474 }
475 }
476
477 if count == 0 {
478 return None;
479 }
480
481 if count > 6 {
482 warnings.push(Warning {
483 source: source.take_normalized_line().item,
484 warning: WarningType::SectionHeadingLevelExceedsMaximum(count - 1),
485 origin: None,
486 });
487
488 return None;
489 }
490
491 let syntactic_level = (count - 1) as i32;
496 let effective_level = syntactic_level.saturating_add(offset);
497
498 if syntactic_level == 0 && effective_level < MIN_SECTION_LEVEL {
505 warnings.push(Warning {
506 source: source.take_normalized_line().item,
507 warning: WarningType::Level0SectionHeadingNotSupported,
508 origin: None,
509 });
510
511 return None;
512 }
513
514 let title = line.take_required_whitespace()?;
518
519 let level = if effective_level < MIN_SECTION_LEVEL {
523 warnings.push(Warning {
524 source: source.take_normalized_line().item,
525 warning: WarningType::SectionHeadingLevelOutOfRange(
526 effective_level,
527 MIN_SECTION_LEVEL as usize,
528 ),
529 origin: None,
530 });
531 MIN_SECTION_LEVEL as usize
532 } else if effective_level > MAX_SECTION_LEVEL {
533 warnings.push(Warning {
534 source: source.take_normalized_line().item,
535 warning: WarningType::SectionHeadingLevelOutOfRange(
536 effective_level,
537 MAX_SECTION_LEVEL as usize,
538 ),
539 origin: None,
540 });
541 MAX_SECTION_LEVEL as usize
542 } else {
543 effective_level as usize
544 };
545
546 Some(MatchedItem {
547 item: (level, title.after),
548 after: mi.after,
549 })
550}
551
552fn peer_or_ancestor_section<'src>(
553 source: Span<'src>,
554 level: usize,
555 most_recent_level: &mut usize,
556 warnings: &mut Vec<Warning<'src>>,
557 parser: &Parser,
558) -> bool {
559 let mut temp_parser = Parser::default();
563
564 let block_metadata_maw = BlockMetadata::parse(source, &mut temp_parser);
565
566 let block_metadata = block_metadata_maw.item;
567 if block_metadata.is_discrete() {
568 return false;
569 }
570
571 let source_after_metadata = block_metadata.block_start;
572
573 let mut ignored_warnings = vec![];
587 if let Some(mi) = parse_title_line(
588 source_after_metadata,
589 parser.level_offset(),
590 &mut ignored_warnings,
591 ) {
592 let found_level = mi.item.0;
593
594 if found_level > *most_recent_level + 1 {
595 warnings.push(Warning {
596 source: source.take_normalized_line().item,
597 warning: WarningType::SectionHeadingLevelSkipped(*most_recent_level, found_level),
598 origin: None,
599 });
600 }
601
602 *most_recent_level = found_level;
603
604 found_level <= level
605 } else {
606 false
607 }
608}
609
610fn generate_section_id(title: &str, parser: &Parser) -> String {
620 let idprefix = parser
621 .attribute_value("idprefix")
622 .as_maybe_str()
623 .unwrap_or_default()
624 .to_owned();
625
626 let idseparator = parser
627 .attribute_value("idseparator")
628 .as_maybe_str()
629 .unwrap_or_default()
630 .to_owned();
631
632 let mut gen_id = title.to_lowercase().to_owned();
633
634 #[allow(clippy::unwrap_used)]
635 static INVALID_SECTION_ID_CHARS: LazyLock<Regex> = LazyLock::new(|| {
636 Regex::new(
637 r"<[^>]+>|<[^&]*>|&(?:[a-z][a-z]+\d{0,2}|#\d{2,5}|#x[\da-f]{2,4});|[^ \w\-.]+",
638 )
639 .unwrap()
640 });
641
642 gen_id = INVALID_SECTION_ID_CHARS
643 .replace_all(&gen_id, "")
644 .to_string();
645
646 let sep = idseparator
648 .chars()
649 .next()
650 .map(|s| s.to_string())
651 .unwrap_or_default();
652
653 gen_id = gen_id.replace([' ', '.', '-'], &sep);
654
655 if !sep.is_empty() {
656 while gen_id.contains(&format!("{}{}", sep, sep)) {
657 gen_id = gen_id.replace(&format!("{}{}", sep, sep), &sep);
658 }
659
660 if gen_id.ends_with(&sep) {
661 gen_id.pop();
662 }
663
664 if idprefix.is_empty() && gen_id.starts_with(&sep) {
665 gen_id = gen_id[sep.len()..].to_string();
666 }
667 }
668
669 format!("{idprefix}{gen_id}")
670}
671
672#[derive(Clone, Copy, Default, Eq, PartialEq)]
678pub enum SectionType {
679 #[default]
681 Normal,
682
683 Appendix,
685
686 Discrete,
689}
690
691impl std::fmt::Debug for SectionType {
692 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693 match self {
694 SectionType::Normal => write!(f, "SectionType::Normal"),
695 SectionType::Appendix => write!(f, "SectionType::Appendix"),
696 SectionType::Discrete => write!(f, "SectionType::Discrete"),
697 }
698 }
699}
700
701#[derive(Clone, Default, Eq, PartialEq)]
708pub struct SectionNumber {
709 pub(crate) section_type: SectionType,
710 pub(crate) components: Vec<usize>,
711}
712
713impl SectionNumber {
714 pub(crate) fn assign_next_number(&mut self, level: usize) {
719 self.components.truncate(level);
721
722 if self.components.len() < level {
723 self.components.resize(level, 1);
724 } else if level > 0
725 && let Some(component) = self.components.get_mut(level - 1)
726 {
727 *component += 1;
728 }
729 }
730
731 pub fn components(&self) -> &[usize] {
733 &self.components
734 }
735}
736
737impl fmt::Display for SectionNumber {
738 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739 f.write_str(
740 &self
741 .components
742 .iter()
743 .enumerate()
744 .map(|(index, x)| {
745 if index == 0 && self.section_type == SectionType::Appendix {
746 char::from_u32(b'A' as u32 + (x - 1) as u32)
747 .unwrap_or('?')
748 .to_string()
749 } else {
750 x.to_string()
751 }
752 })
753 .collect::<Vec<String>>()
754 .join("."),
755 )
756 }
757}
758
759impl fmt::Debug for SectionNumber {
760 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
761 f.debug_struct("SectionNumber")
762 .field("section_type", &self.section_type)
763 .field("components", &DebugSliceReference(&self.components))
764 .finish()
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 #![allow(clippy::panic)]
771 #![allow(clippy::unwrap_used)]
772
773 use crate::{
774 blocks::{metadata::BlockMetadata, section::SectionType},
775 tests::prelude::*,
776 };
777
778 #[test]
779 fn impl_clone() {
780 let mut parser = Parser::default();
782 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
783
784 let b1 = crate::blocks::SectionBlock::parse(
785 &BlockMetadata::new("== Section Title"),
786 &mut parser,
787 &mut warnings,
788 )
789 .unwrap();
790
791 let b2 = b1.item.clone();
792 assert_eq!(b1.item, b2);
793 }
794
795 #[test]
796 fn err_empty_source() {
797 let mut parser = Parser::default();
798 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
799
800 assert!(
801 crate::blocks::SectionBlock::parse(&BlockMetadata::new(""), &mut parser, &mut warnings)
802 .is_none()
803 );
804 }
805
806 #[test]
807 fn err_only_spaces() {
808 let mut parser = Parser::default();
809 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
810
811 assert!(
812 crate::blocks::SectionBlock::parse(
813 &BlockMetadata::new(" "),
814 &mut parser,
815 &mut warnings
816 )
817 .is_none()
818 );
819 }
820
821 #[test]
822 fn err_not_section() {
823 let mut parser = Parser::default();
824 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
825
826 assert!(
827 crate::blocks::SectionBlock::parse(
828 &BlockMetadata::new("blah blah"),
829 &mut parser,
830 &mut warnings
831 )
832 .is_none()
833 );
834 }
835
836 mod asciidoc_style_headers {
837 use std::ops::Deref;
838
839 use crate::{
840 blocks::{ContentModel, MediaType, metadata::BlockMetadata, section::SectionType},
841 tests::prelude::*,
842 };
843
844 #[test]
845 fn err_missing_space_before_title() {
846 let mut parser = Parser::default();
847 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
848
849 assert!(
850 crate::blocks::SectionBlock::parse(
851 &BlockMetadata::new("=blah blah"),
852 &mut parser,
853 &mut warnings
854 )
855 .is_none()
856 );
857 }
858
859 #[test]
860 fn simplest_section_block() {
861 let mut parser = Parser::default();
862 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
863
864 let mi = crate::blocks::SectionBlock::parse(
865 &BlockMetadata::new("== Section Title"),
866 &mut parser,
867 &mut warnings,
868 )
869 .unwrap();
870
871 assert_eq!(mi.item.content_model(), ContentModel::Compound);
872 assert_eq!(mi.item.raw_context().deref(), "section");
873 assert_eq!(mi.item.resolved_context().deref(), "section");
874 assert!(mi.item.declared_style().is_none());
875 assert_eq!(mi.item.id().unwrap(), "_section_title");
876 assert!(mi.item.roles().is_empty());
877 assert!(mi.item.options().is_empty());
878 assert!(mi.item.title_source().is_none());
879 assert!(mi.item.title().is_none());
880 assert!(mi.item.anchor().is_none());
881 assert!(mi.item.anchor_reftext().is_none());
882 assert!(mi.item.attrlist().is_none());
883 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
884
885 assert_eq!(
886 mi.item,
887 SectionBlock {
888 level: 1,
889 section_title: Content {
890 original: Span {
891 data: "Section Title",
892 line: 1,
893 col: 4,
894 offset: 3,
895 },
896 rendered: "Section Title",
897 },
898 blocks: &[],
899 source: Span {
900 data: "== Section Title",
901 line: 1,
902 col: 1,
903 offset: 0,
904 },
905 title_source: None,
906 title: None,
907 anchor: None,
908 anchor_reftext: None,
909 attrlist: None,
910 section_type: SectionType::Normal,
911 section_id: Some("_section_title"),
912 caption: None,
913 section_number: None,
914 }
915 );
916
917 assert_eq!(
918 mi.after,
919 Span {
920 data: "",
921 line: 1,
922 col: 17,
923 offset: 16
924 }
925 );
926 }
927
928 #[test]
929 fn has_child_block() {
930 let mut parser = Parser::default();
931 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
932
933 let mi = crate::blocks::SectionBlock::parse(
934 &BlockMetadata::new("== Section Title\n\nabc"),
935 &mut parser,
936 &mut warnings,
937 )
938 .unwrap();
939
940 assert_eq!(mi.item.content_model(), ContentModel::Compound);
941 assert_eq!(mi.item.raw_context().deref(), "section");
942 assert_eq!(mi.item.resolved_context().deref(), "section");
943 assert!(mi.item.declared_style().is_none());
944 assert_eq!(mi.item.id().unwrap(), "_section_title");
945 assert!(mi.item.roles().is_empty());
946 assert!(mi.item.options().is_empty());
947 assert!(mi.item.title_source().is_none());
948 assert!(mi.item.title().is_none());
949 assert!(mi.item.anchor().is_none());
950 assert!(mi.item.anchor_reftext().is_none());
951 assert!(mi.item.attrlist().is_none());
952 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
953
954 assert_eq!(
955 mi.item,
956 SectionBlock {
957 level: 1,
958 section_title: Content {
959 original: Span {
960 data: "Section Title",
961 line: 1,
962 col: 4,
963 offset: 3,
964 },
965 rendered: "Section Title",
966 },
967 blocks: &[Block::Simple(SimpleBlock {
968 content: Content {
969 original: Span {
970 data: "abc",
971 line: 3,
972 col: 1,
973 offset: 18,
974 },
975 rendered: "abc",
976 },
977 source: Span {
978 data: "abc",
979 line: 3,
980 col: 1,
981 offset: 18,
982 },
983 style: SimpleBlockStyle::Paragraph,
984 title_source: None,
985 title: None,
986 caption: None,
987 number: None,
988 anchor: None,
989 anchor_reftext: None,
990 attrlist: None,
991 })],
992 source: Span {
993 data: "== Section Title\n\nabc",
994 line: 1,
995 col: 1,
996 offset: 0,
997 },
998 title_source: None,
999 title: None,
1000 anchor: None,
1001 anchor_reftext: None,
1002 attrlist: None,
1003 section_type: SectionType::Normal,
1004 section_id: Some("_section_title"),
1005 caption: None,
1006 section_number: None,
1007 }
1008 );
1009
1010 assert_eq!(
1011 mi.after,
1012 Span {
1013 data: "",
1014 line: 3,
1015 col: 4,
1016 offset: 21
1017 }
1018 );
1019 }
1020
1021 #[test]
1022 fn has_macro_block_with_extra_blank_line() {
1023 let mut parser = Parser::default();
1024 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1025
1026 let mi = crate::blocks::SectionBlock::parse(
1027 &BlockMetadata::new(
1028 "== Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]\n\n",
1029 ),
1030 &mut parser,
1031 &mut warnings,
1032 )
1033 .unwrap();
1034
1035 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1036 assert_eq!(mi.item.raw_context().deref(), "section");
1037 assert_eq!(mi.item.resolved_context().deref(), "section");
1038 assert!(mi.item.declared_style().is_none());
1039 assert_eq!(mi.item.id().unwrap(), "_section_title");
1040 assert!(mi.item.roles().is_empty());
1041 assert!(mi.item.options().is_empty());
1042 assert!(mi.item.title_source().is_none());
1043 assert!(mi.item.title().is_none());
1044 assert!(mi.item.anchor().is_none());
1045 assert!(mi.item.anchor_reftext().is_none());
1046 assert!(mi.item.attrlist().is_none());
1047 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1048
1049 assert_eq!(
1050 mi.item,
1051 SectionBlock {
1052 level: 1,
1053 section_title: Content {
1054 original: Span {
1055 data: "Section Title",
1056 line: 1,
1057 col: 4,
1058 offset: 3,
1059 },
1060 rendered: "Section Title",
1061 },
1062 blocks: &[Block::Media(MediaBlock {
1063 type_: MediaType::Image,
1064 target: Span {
1065 data: "bar",
1066 line: 3,
1067 col: 8,
1068 offset: 25,
1069 },
1070 macro_attrlist: Attrlist {
1071 attributes: &[
1072 ElementAttribute {
1073 name: Some("alt"),
1074 shorthand_items: &[],
1075 value: "Sunset"
1076 },
1077 ElementAttribute {
1078 name: Some("width"),
1079 shorthand_items: &[],
1080 value: "300"
1081 },
1082 ElementAttribute {
1083 name: Some("height"),
1084 shorthand_items: &[],
1085 value: "400"
1086 }
1087 ],
1088 anchor: None,
1089 source: Span {
1090 data: "alt=Sunset,width=300,height=400",
1091 line: 3,
1092 col: 12,
1093 offset: 29,
1094 }
1095 },
1096 source: Span {
1097 data: "image::bar[alt=Sunset,width=300,height=400]",
1098 line: 3,
1099 col: 1,
1100 offset: 18,
1101 },
1102 title_source: None,
1103 title: None,
1104 caption: None,
1105 number: None,
1106 anchor: None,
1107 anchor_reftext: None,
1108 attrlist: None,
1109 })],
1110 source: Span {
1111 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]",
1112 line: 1,
1113 col: 1,
1114 offset: 0,
1115 },
1116 title_source: None,
1117 title: None,
1118 anchor: None,
1119 anchor_reftext: None,
1120 attrlist: None,
1121 section_type: SectionType::Normal,
1122 section_id: Some("_section_title"),
1123 caption: None,
1124 section_number: None,
1125 }
1126 );
1127
1128 assert_eq!(
1129 mi.after,
1130 Span {
1131 data: "",
1132 line: 5,
1133 col: 1,
1134 offset: 63
1135 }
1136 );
1137 }
1138
1139 #[test]
1140 fn has_child_block_with_errors() {
1141 let mut parser = Parser::default();
1142 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1143
1144 let mi = crate::blocks::SectionBlock::parse(
1145 &BlockMetadata::new(
1146 "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1147 ),
1148 &mut parser,
1149 &mut warnings,
1150 )
1151 .unwrap();
1152
1153 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1154 assert_eq!(mi.item.raw_context().deref(), "section");
1155 assert_eq!(mi.item.resolved_context().deref(), "section");
1156 assert!(mi.item.declared_style().is_none());
1157 assert_eq!(mi.item.id().unwrap(), "_section_title");
1158 assert!(mi.item.roles().is_empty());
1159 assert!(mi.item.options().is_empty());
1160 assert!(mi.item.title_source().is_none());
1161 assert!(mi.item.title().is_none());
1162 assert!(mi.item.anchor().is_none());
1163 assert!(mi.item.anchor_reftext().is_none());
1164 assert!(mi.item.attrlist().is_none());
1165 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1166
1167 assert_eq!(
1168 mi.item,
1169 SectionBlock {
1170 level: 1,
1171 section_title: Content {
1172 original: Span {
1173 data: "Section Title",
1174 line: 1,
1175 col: 4,
1176 offset: 3,
1177 },
1178 rendered: "Section Title",
1179 },
1180 blocks: &[Block::Media(MediaBlock {
1181 type_: MediaType::Image,
1182 target: Span {
1183 data: "bar",
1184 line: 3,
1185 col: 8,
1186 offset: 25,
1187 },
1188 macro_attrlist: Attrlist {
1189 attributes: &[
1190 ElementAttribute {
1191 name: Some("alt"),
1192 shorthand_items: &[],
1193 value: "Sunset"
1194 },
1195 ElementAttribute {
1196 name: Some("width"),
1197 shorthand_items: &[],
1198 value: "300"
1199 },
1200 ElementAttribute {
1201 name: Some("height"),
1202 shorthand_items: &[],
1203 value: "400"
1204 }
1205 ],
1206 anchor: None,
1207 source: Span {
1208 data: "alt=Sunset,width=300,,height=400",
1209 line: 3,
1210 col: 12,
1211 offset: 29,
1212 }
1213 },
1214 source: Span {
1215 data: "image::bar[alt=Sunset,width=300,,height=400]",
1216 line: 3,
1217 col: 1,
1218 offset: 18,
1219 },
1220 title_source: None,
1221 title: None,
1222 caption: None,
1223 number: None,
1224 anchor: None,
1225 anchor_reftext: None,
1226 attrlist: None,
1227 })],
1228 source: Span {
1229 data: "== Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
1230 line: 1,
1231 col: 1,
1232 offset: 0,
1233 },
1234 title_source: None,
1235 title: None,
1236 anchor: None,
1237 anchor_reftext: None,
1238 attrlist: None,
1239 section_type: SectionType::Normal,
1240 section_id: Some("_section_title"),
1241 caption: None,
1242 section_number: None,
1243 }
1244 );
1245
1246 assert_eq!(
1247 mi.after,
1248 Span {
1249 data: "",
1250 line: 3,
1251 col: 45,
1252 offset: 62
1253 }
1254 );
1255
1256 assert_eq!(
1257 warnings,
1258 vec![Warning {
1259 source: Span {
1260 data: "alt=Sunset,width=300,,height=400",
1261 line: 3,
1262 col: 12,
1263 offset: 29,
1264 },
1265 warning: WarningType::EmptyAttributeValue,
1266 }]
1267 );
1268 }
1269
1270 #[test]
1271 fn dont_stop_at_child_section() {
1272 let mut parser = Parser::default();
1273 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1274
1275 let mi = crate::blocks::SectionBlock::parse(
1276 &BlockMetadata::new("== Section Title\n\nabc\n\n=== Section 2\n\ndef"),
1277 &mut parser,
1278 &mut warnings,
1279 )
1280 .unwrap();
1281
1282 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1283 assert_eq!(mi.item.raw_context().deref(), "section");
1284 assert_eq!(mi.item.resolved_context().deref(), "section");
1285 assert!(mi.item.declared_style().is_none());
1286 assert_eq!(mi.item.id().unwrap(), "_section_title");
1287 assert!(mi.item.roles().is_empty());
1288 assert!(mi.item.options().is_empty());
1289 assert!(mi.item.title_source().is_none());
1290 assert!(mi.item.title().is_none());
1291 assert!(mi.item.anchor().is_none());
1292 assert!(mi.item.anchor_reftext().is_none());
1293 assert!(mi.item.attrlist().is_none());
1294 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1295
1296 assert_eq!(
1297 mi.item,
1298 SectionBlock {
1299 level: 1,
1300 section_title: Content {
1301 original: Span {
1302 data: "Section Title",
1303 line: 1,
1304 col: 4,
1305 offset: 3,
1306 },
1307 rendered: "Section Title",
1308 },
1309 blocks: &[
1310 Block::Simple(SimpleBlock {
1311 content: Content {
1312 original: Span {
1313 data: "abc",
1314 line: 3,
1315 col: 1,
1316 offset: 18,
1317 },
1318 rendered: "abc",
1319 },
1320 source: Span {
1321 data: "abc",
1322 line: 3,
1323 col: 1,
1324 offset: 18,
1325 },
1326 style: SimpleBlockStyle::Paragraph,
1327 title_source: None,
1328 title: None,
1329 caption: None,
1330 number: None,
1331 anchor: None,
1332 anchor_reftext: None,
1333 attrlist: None,
1334 }),
1335 Block::Section(SectionBlock {
1336 level: 2,
1337 section_title: Content {
1338 original: Span {
1339 data: "Section 2",
1340 line: 5,
1341 col: 5,
1342 offset: 27,
1343 },
1344 rendered: "Section 2",
1345 },
1346 blocks: &[Block::Simple(SimpleBlock {
1347 content: Content {
1348 original: Span {
1349 data: "def",
1350 line: 7,
1351 col: 1,
1352 offset: 38,
1353 },
1354 rendered: "def",
1355 },
1356 source: Span {
1357 data: "def",
1358 line: 7,
1359 col: 1,
1360 offset: 38,
1361 },
1362 style: SimpleBlockStyle::Paragraph,
1363 title_source: None,
1364 title: None,
1365 caption: None,
1366 number: None,
1367 anchor: None,
1368 anchor_reftext: None,
1369 attrlist: None,
1370 })],
1371 source: Span {
1372 data: "=== Section 2\n\ndef",
1373 line: 5,
1374 col: 1,
1375 offset: 23,
1376 },
1377 title_source: None,
1378 title: None,
1379 anchor: None,
1380 anchor_reftext: None,
1381 attrlist: None,
1382 section_type: SectionType::Normal,
1383 section_id: Some("_section_2"),
1384 caption: None,
1385 section_number: None,
1386 })
1387 ],
1388 source: Span {
1389 data: "== Section Title\n\nabc\n\n=== Section 2\n\ndef",
1390 line: 1,
1391 col: 1,
1392 offset: 0,
1393 },
1394 title_source: None,
1395 title: None,
1396 anchor: None,
1397 anchor_reftext: None,
1398 attrlist: None,
1399 section_type: SectionType::Normal,
1400 section_id: Some("_section_title"),
1401 caption: None,
1402 section_number: None,
1403 }
1404 );
1405
1406 assert_eq!(
1407 mi.after,
1408 Span {
1409 data: "",
1410 line: 7,
1411 col: 4,
1412 offset: 41
1413 }
1414 );
1415 }
1416
1417 #[test]
1418 fn stop_at_peer_section() {
1419 let mut parser = Parser::default();
1420 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1421
1422 let mi = crate::blocks::SectionBlock::parse(
1423 &BlockMetadata::new("== Section Title\n\nabc\n\n== Section 2\n\ndef"),
1424 &mut parser,
1425 &mut warnings,
1426 )
1427 .unwrap();
1428
1429 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1430 assert_eq!(mi.item.raw_context().deref(), "section");
1431 assert_eq!(mi.item.resolved_context().deref(), "section");
1432 assert!(mi.item.declared_style().is_none());
1433 assert_eq!(mi.item.id().unwrap(), "_section_title");
1434 assert!(mi.item.roles().is_empty());
1435 assert!(mi.item.options().is_empty());
1436 assert!(mi.item.title_source().is_none());
1437 assert!(mi.item.title().is_none());
1438 assert!(mi.item.anchor().is_none());
1439 assert!(mi.item.anchor_reftext().is_none());
1440 assert!(mi.item.attrlist().is_none());
1441 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1442
1443 assert_eq!(
1444 mi.item,
1445 SectionBlock {
1446 level: 1,
1447 section_title: Content {
1448 original: Span {
1449 data: "Section Title",
1450 line: 1,
1451 col: 4,
1452 offset: 3,
1453 },
1454 rendered: "Section Title",
1455 },
1456 blocks: &[Block::Simple(SimpleBlock {
1457 content: Content {
1458 original: Span {
1459 data: "abc",
1460 line: 3,
1461 col: 1,
1462 offset: 18,
1463 },
1464 rendered: "abc",
1465 },
1466 source: Span {
1467 data: "abc",
1468 line: 3,
1469 col: 1,
1470 offset: 18,
1471 },
1472 style: SimpleBlockStyle::Paragraph,
1473 title_source: None,
1474 title: None,
1475 caption: None,
1476 number: None,
1477 anchor: None,
1478 anchor_reftext: None,
1479 attrlist: None,
1480 })],
1481 source: Span {
1482 data: "== Section Title\n\nabc",
1483 line: 1,
1484 col: 1,
1485 offset: 0,
1486 },
1487 title_source: None,
1488 title: None,
1489 anchor: None,
1490 anchor_reftext: None,
1491 attrlist: None,
1492 section_type: SectionType::Normal,
1493 section_id: Some("_section_title"),
1494 caption: None,
1495 section_number: None,
1496 }
1497 );
1498
1499 assert_eq!(
1500 mi.after,
1501 Span {
1502 data: "== Section 2\n\ndef",
1503 line: 5,
1504 col: 1,
1505 offset: 23
1506 }
1507 );
1508 }
1509
1510 #[test]
1511 fn stop_at_ancestor_section() {
1512 let mut parser = Parser::default();
1513 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1514
1515 let mi = crate::blocks::SectionBlock::parse(
1516 &BlockMetadata::new("=== Section Title\n\nabc\n\n== Section 2\n\ndef"),
1517 &mut parser,
1518 &mut warnings,
1519 )
1520 .unwrap();
1521
1522 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1523 assert_eq!(mi.item.raw_context().deref(), "section");
1524 assert_eq!(mi.item.resolved_context().deref(), "section");
1525 assert!(mi.item.declared_style().is_none());
1526 assert_eq!(mi.item.id().unwrap(), "_section_title");
1527 assert!(mi.item.roles().is_empty());
1528 assert!(mi.item.options().is_empty());
1529 assert!(mi.item.title_source().is_none());
1530 assert!(mi.item.title().is_none());
1531 assert!(mi.item.anchor().is_none());
1532 assert!(mi.item.anchor_reftext().is_none());
1533 assert!(mi.item.attrlist().is_none());
1534 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1535
1536 assert_eq!(
1537 mi.item,
1538 SectionBlock {
1539 level: 2,
1540 section_title: Content {
1541 original: Span {
1542 data: "Section Title",
1543 line: 1,
1544 col: 5,
1545 offset: 4,
1546 },
1547 rendered: "Section Title",
1548 },
1549 blocks: &[Block::Simple(SimpleBlock {
1550 content: Content {
1551 original: Span {
1552 data: "abc",
1553 line: 3,
1554 col: 1,
1555 offset: 19,
1556 },
1557 rendered: "abc",
1558 },
1559 source: Span {
1560 data: "abc",
1561 line: 3,
1562 col: 1,
1563 offset: 19,
1564 },
1565 style: SimpleBlockStyle::Paragraph,
1566 title_source: None,
1567 title: None,
1568 caption: None,
1569 number: None,
1570 anchor: None,
1571 anchor_reftext: None,
1572 attrlist: None,
1573 })],
1574 source: Span {
1575 data: "=== Section Title\n\nabc",
1576 line: 1,
1577 col: 1,
1578 offset: 0,
1579 },
1580 title_source: None,
1581 title: None,
1582 anchor: None,
1583 anchor_reftext: None,
1584 attrlist: None,
1585 section_type: SectionType::Normal,
1586 section_id: Some("_section_title"),
1587 caption: None,
1588 section_number: None,
1589 }
1590 );
1591
1592 assert_eq!(
1593 mi.after,
1594 Span {
1595 data: "== Section 2\n\ndef",
1596 line: 5,
1597 col: 1,
1598 offset: 24
1599 }
1600 );
1601 }
1602
1603 #[test]
1604 fn section_title_with_markup() {
1605 let mut parser = Parser::default();
1606 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1607
1608 let mi = crate::blocks::SectionBlock::parse(
1609 &BlockMetadata::new("== Section with *bold* text"),
1610 &mut parser,
1611 &mut warnings,
1612 )
1613 .unwrap();
1614
1615 assert_eq!(
1616 mi.item.section_title_source(),
1617 Span {
1618 data: "Section with *bold* text",
1619 line: 1,
1620 col: 4,
1621 offset: 3,
1622 }
1623 );
1624
1625 assert_eq!(
1626 mi.item.section_title(),
1627 "Section with <strong>bold</strong> text"
1628 );
1629
1630 assert_eq!(mi.item.section_type(), SectionType::Normal);
1631 assert_eq!(mi.item.id().unwrap(), "_section_with_bold_text");
1632 }
1633
1634 #[test]
1635 fn section_title_with_special_chars() {
1636 let mut parser = Parser::default();
1637 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1638
1639 let mi = crate::blocks::SectionBlock::parse(
1640 &BlockMetadata::new("== Section with <brackets> & ampersands"),
1641 &mut parser,
1642 &mut warnings,
1643 )
1644 .unwrap();
1645
1646 assert_eq!(
1647 mi.item.section_title_source(),
1648 Span {
1649 data: "Section with <brackets> & ampersands",
1650 line: 1,
1651 col: 4,
1652 offset: 3,
1653 }
1654 );
1655
1656 assert_eq!(
1657 mi.item.section_title(),
1658 "Section with <brackets> & ampersands"
1659 );
1660
1661 assert_eq!(mi.item.id().unwrap(), "_section_with_ampersands");
1662 }
1663
1664 #[test]
1665 fn err_level_0_section_heading() {
1666 let mut parser = Parser::default();
1667 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1668
1669 let result = crate::blocks::SectionBlock::parse(
1670 &BlockMetadata::new("= Document Title"),
1671 &mut parser,
1672 &mut warnings,
1673 );
1674
1675 assert!(result.is_none());
1676
1677 assert_eq!(
1678 warnings,
1679 vec![Warning {
1680 source: Span {
1681 data: "= Document Title",
1682 line: 1,
1683 col: 1,
1684 offset: 0,
1685 },
1686 warning: WarningType::Level0SectionHeadingNotSupported,
1687 }]
1688 );
1689 }
1690
1691 #[test]
1692 fn err_section_heading_level_exceeds_maximum() {
1693 let mut parser = Parser::default();
1694 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1695
1696 let result = crate::blocks::SectionBlock::parse(
1697 &BlockMetadata::new("======= Level 6 Section"),
1698 &mut parser,
1699 &mut warnings,
1700 );
1701
1702 assert!(result.is_none());
1703
1704 assert_eq!(
1705 warnings,
1706 vec![Warning {
1707 source: Span {
1708 data: "======= Level 6 Section",
1709 line: 1,
1710 col: 1,
1711 offset: 0,
1712 },
1713 warning: WarningType::SectionHeadingLevelExceedsMaximum(6),
1714 }]
1715 );
1716 }
1717
1718 #[test]
1719 fn valid_maximum_level_5_section() {
1720 let mut parser = Parser::default();
1721 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1722
1723 let mi = crate::blocks::SectionBlock::parse(
1724 &BlockMetadata::new("====== Level 5 Section"),
1725 &mut parser,
1726 &mut warnings,
1727 )
1728 .unwrap();
1729
1730 assert!(warnings.is_empty());
1731
1732 assert_eq!(mi.item.level(), 5);
1733 assert_eq!(mi.item.section_title(), "Level 5 Section");
1734 assert_eq!(mi.item.section_type(), SectionType::Normal);
1735 assert_eq!(mi.item.id().unwrap(), "_level_5_section");
1736 }
1737
1738 #[test]
1739 fn warn_section_level_skipped() {
1740 let mut parser = Parser::default();
1741 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1742
1743 let mi = crate::blocks::SectionBlock::parse(
1744 &BlockMetadata::new("== Level 1\n\n==== Level 3 (skipped level 2)"),
1745 &mut parser,
1746 &mut warnings,
1747 )
1748 .unwrap();
1749
1750 assert_eq!(mi.item.level(), 1);
1751 assert_eq!(mi.item.section_title(), "Level 1");
1752 assert_eq!(mi.item.section_type(), SectionType::Normal);
1753 assert_eq!(mi.item.nested_blocks().len(), 1);
1754 assert_eq!(mi.item.id().unwrap(), "_level_1");
1755
1756 assert_eq!(
1757 warnings,
1758 vec![Warning {
1759 source: Span {
1760 data: "==== Level 3 (skipped level 2)",
1761 line: 3,
1762 col: 1,
1763 offset: 12,
1764 },
1765 warning: WarningType::SectionHeadingLevelSkipped(1, 3),
1766 }]
1767 );
1768 }
1769 }
1770
1771 mod markdown_style_headings {
1772 use std::ops::Deref;
1773
1774 use crate::{
1775 blocks::{ContentModel, MediaType, metadata::BlockMetadata, section::SectionType},
1776 tests::prelude::*,
1777 };
1778
1779 #[test]
1780 fn err_missing_space_before_title() {
1781 let mut parser = Parser::default();
1782 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1783
1784 assert!(
1785 crate::blocks::SectionBlock::parse(
1786 &BlockMetadata::new("#blah blah"),
1787 &mut parser,
1788 &mut warnings
1789 )
1790 .is_none()
1791 );
1792 }
1793
1794 #[test]
1795 fn simplest_section_block() {
1796 let mut parser = Parser::default();
1797 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1798
1799 let mi = crate::blocks::SectionBlock::parse(
1800 &BlockMetadata::new("## Section Title"),
1801 &mut parser,
1802 &mut warnings,
1803 )
1804 .unwrap();
1805
1806 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1807 assert_eq!(mi.item.raw_context().deref(), "section");
1808 assert_eq!(mi.item.resolved_context().deref(), "section");
1809 assert!(mi.item.declared_style().is_none());
1810 assert_eq!(mi.item.id().unwrap(), "_section_title");
1811 assert!(mi.item.roles().is_empty());
1812 assert!(mi.item.options().is_empty());
1813 assert!(mi.item.title_source().is_none());
1814 assert!(mi.item.title().is_none());
1815 assert!(mi.item.anchor().is_none());
1816 assert!(mi.item.anchor_reftext().is_none());
1817 assert!(mi.item.attrlist().is_none());
1818 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1819
1820 assert_eq!(
1821 mi.item,
1822 SectionBlock {
1823 level: 1,
1824 section_title: Content {
1825 original: Span {
1826 data: "Section Title",
1827 line: 1,
1828 col: 4,
1829 offset: 3,
1830 },
1831 rendered: "Section Title",
1832 },
1833 blocks: &[],
1834 source: Span {
1835 data: "## Section Title",
1836 line: 1,
1837 col: 1,
1838 offset: 0,
1839 },
1840 title_source: None,
1841 title: None,
1842 anchor: None,
1843 anchor_reftext: None,
1844 attrlist: None,
1845 section_type: SectionType::Normal,
1846 section_id: Some("_section_title"),
1847 caption: None,
1848 section_number: None,
1849 }
1850 );
1851
1852 assert_eq!(
1853 mi.after,
1854 Span {
1855 data: "",
1856 line: 1,
1857 col: 17,
1858 offset: 16
1859 }
1860 );
1861 }
1862
1863 #[test]
1864 fn has_child_block() {
1865 let mut parser = Parser::default();
1866 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1867
1868 let mi = crate::blocks::SectionBlock::parse(
1869 &BlockMetadata::new("## Section Title\n\nabc"),
1870 &mut parser,
1871 &mut warnings,
1872 )
1873 .unwrap();
1874
1875 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1876 assert_eq!(mi.item.raw_context().deref(), "section");
1877 assert_eq!(mi.item.resolved_context().deref(), "section");
1878 assert!(mi.item.declared_style().is_none());
1879 assert_eq!(mi.item.id().unwrap(), "_section_title");
1880 assert!(mi.item.roles().is_empty());
1881 assert!(mi.item.options().is_empty());
1882 assert!(mi.item.title_source().is_none());
1883 assert!(mi.item.title().is_none());
1884 assert!(mi.item.anchor().is_none());
1885 assert!(mi.item.anchor_reftext().is_none());
1886 assert!(mi.item.attrlist().is_none());
1887 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1888
1889 assert_eq!(
1890 mi.item,
1891 SectionBlock {
1892 level: 1,
1893 section_title: Content {
1894 original: Span {
1895 data: "Section Title",
1896 line: 1,
1897 col: 4,
1898 offset: 3,
1899 },
1900 rendered: "Section Title",
1901 },
1902 blocks: &[Block::Simple(SimpleBlock {
1903 content: Content {
1904 original: Span {
1905 data: "abc",
1906 line: 3,
1907 col: 1,
1908 offset: 18,
1909 },
1910 rendered: "abc",
1911 },
1912 source: Span {
1913 data: "abc",
1914 line: 3,
1915 col: 1,
1916 offset: 18,
1917 },
1918 style: SimpleBlockStyle::Paragraph,
1919 title_source: None,
1920 title: None,
1921 caption: None,
1922 number: None,
1923 anchor: None,
1924 anchor_reftext: None,
1925 attrlist: None,
1926 })],
1927 source: Span {
1928 data: "## Section Title\n\nabc",
1929 line: 1,
1930 col: 1,
1931 offset: 0,
1932 },
1933 title_source: None,
1934 title: None,
1935 anchor: None,
1936 anchor_reftext: None,
1937 attrlist: None,
1938 section_type: SectionType::Normal,
1939 section_id: Some("_section_title"),
1940 caption: None,
1941 section_number: None,
1942 }
1943 );
1944
1945 assert_eq!(
1946 mi.after,
1947 Span {
1948 data: "",
1949 line: 3,
1950 col: 4,
1951 offset: 21
1952 }
1953 );
1954 }
1955
1956 #[test]
1957 fn has_macro_block_with_extra_blank_line() {
1958 let mut parser = Parser::default();
1959 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
1960
1961 let mi = crate::blocks::SectionBlock::parse(
1962 &BlockMetadata::new(
1963 "## Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]\n\n",
1964 ),
1965 &mut parser,
1966 &mut warnings,
1967 )
1968 .unwrap();
1969
1970 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1971 assert_eq!(mi.item.raw_context().deref(), "section");
1972 assert_eq!(mi.item.resolved_context().deref(), "section");
1973 assert!(mi.item.declared_style().is_none());
1974 assert_eq!(mi.item.id().unwrap(), "_section_title");
1975 assert!(mi.item.roles().is_empty());
1976 assert!(mi.item.options().is_empty());
1977 assert!(mi.item.title_source().is_none());
1978 assert!(mi.item.title().is_none());
1979 assert!(mi.item.anchor().is_none());
1980 assert!(mi.item.anchor_reftext().is_none());
1981 assert!(mi.item.attrlist().is_none());
1982 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1983
1984 assert_eq!(
1985 mi.item,
1986 SectionBlock {
1987 level: 1,
1988 section_title: Content {
1989 original: Span {
1990 data: "Section Title",
1991 line: 1,
1992 col: 4,
1993 offset: 3,
1994 },
1995 rendered: "Section Title",
1996 },
1997 blocks: &[Block::Media(MediaBlock {
1998 type_: MediaType::Image,
1999 target: Span {
2000 data: "bar",
2001 line: 3,
2002 col: 8,
2003 offset: 25,
2004 },
2005 macro_attrlist: Attrlist {
2006 attributes: &[
2007 ElementAttribute {
2008 name: Some("alt"),
2009 shorthand_items: &[],
2010 value: "Sunset"
2011 },
2012 ElementAttribute {
2013 name: Some("width"),
2014 shorthand_items: &[],
2015 value: "300"
2016 },
2017 ElementAttribute {
2018 name: Some("height"),
2019 shorthand_items: &[],
2020 value: "400"
2021 }
2022 ],
2023 anchor: None,
2024 source: Span {
2025 data: "alt=Sunset,width=300,height=400",
2026 line: 3,
2027 col: 12,
2028 offset: 29,
2029 }
2030 },
2031 source: Span {
2032 data: "image::bar[alt=Sunset,width=300,height=400]",
2033 line: 3,
2034 col: 1,
2035 offset: 18,
2036 },
2037 title_source: None,
2038 title: None,
2039 caption: None,
2040 number: None,
2041 anchor: None,
2042 anchor_reftext: None,
2043 attrlist: None,
2044 })],
2045 source: Span {
2046 data: "## Section Title\n\nimage::bar[alt=Sunset,width=300,height=400]",
2047 line: 1,
2048 col: 1,
2049 offset: 0,
2050 },
2051 title_source: None,
2052 title: None,
2053 anchor: None,
2054 anchor_reftext: None,
2055 attrlist: None,
2056 section_type: SectionType::Normal,
2057 section_id: Some("_section_title"),
2058 caption: None,
2059 section_number: None,
2060 }
2061 );
2062
2063 assert_eq!(
2064 mi.after,
2065 Span {
2066 data: "",
2067 line: 5,
2068 col: 1,
2069 offset: 63
2070 }
2071 );
2072 }
2073
2074 #[test]
2075 fn has_child_block_with_errors() {
2076 let mut parser = Parser::default();
2077 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2078
2079 let mi = crate::blocks::SectionBlock::parse(
2080 &BlockMetadata::new(
2081 "## Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
2082 ),
2083 &mut parser,
2084 &mut warnings,
2085 )
2086 .unwrap();
2087
2088 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2089 assert_eq!(mi.item.raw_context().deref(), "section");
2090 assert_eq!(mi.item.resolved_context().deref(), "section");
2091 assert!(mi.item.declared_style().is_none());
2092 assert_eq!(mi.item.id().unwrap(), "_section_title");
2093 assert!(mi.item.roles().is_empty());
2094 assert!(mi.item.options().is_empty());
2095 assert!(mi.item.title_source().is_none());
2096 assert!(mi.item.title().is_none());
2097 assert!(mi.item.anchor().is_none());
2098 assert!(mi.item.anchor_reftext().is_none());
2099 assert!(mi.item.attrlist().is_none());
2100 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2101
2102 assert_eq!(
2103 mi.item,
2104 SectionBlock {
2105 level: 1,
2106 section_title: Content {
2107 original: Span {
2108 data: "Section Title",
2109 line: 1,
2110 col: 4,
2111 offset: 3,
2112 },
2113 rendered: "Section Title",
2114 },
2115 blocks: &[Block::Media(MediaBlock {
2116 type_: MediaType::Image,
2117 target: Span {
2118 data: "bar",
2119 line: 3,
2120 col: 8,
2121 offset: 25,
2122 },
2123 macro_attrlist: Attrlist {
2124 attributes: &[
2125 ElementAttribute {
2126 name: Some("alt"),
2127 shorthand_items: &[],
2128 value: "Sunset"
2129 },
2130 ElementAttribute {
2131 name: Some("width"),
2132 shorthand_items: &[],
2133 value: "300"
2134 },
2135 ElementAttribute {
2136 name: Some("height"),
2137 shorthand_items: &[],
2138 value: "400"
2139 }
2140 ],
2141 anchor: None,
2142 source: Span {
2143 data: "alt=Sunset,width=300,,height=400",
2144 line: 3,
2145 col: 12,
2146 offset: 29,
2147 }
2148 },
2149 source: Span {
2150 data: "image::bar[alt=Sunset,width=300,,height=400]",
2151 line: 3,
2152 col: 1,
2153 offset: 18,
2154 },
2155 title_source: None,
2156 title: None,
2157 caption: None,
2158 number: None,
2159 anchor: None,
2160 anchor_reftext: None,
2161 attrlist: None,
2162 })],
2163 source: Span {
2164 data: "## Section Title\n\nimage::bar[alt=Sunset,width=300,,height=400]",
2165 line: 1,
2166 col: 1,
2167 offset: 0,
2168 },
2169 title_source: None,
2170 title: None,
2171 anchor: None,
2172 anchor_reftext: None,
2173 attrlist: None,
2174 section_type: SectionType::Normal,
2175 section_id: Some("_section_title"),
2176 caption: None,
2177 section_number: None,
2178 }
2179 );
2180
2181 assert_eq!(
2182 mi.after,
2183 Span {
2184 data: "",
2185 line: 3,
2186 col: 45,
2187 offset: 62
2188 }
2189 );
2190
2191 assert_eq!(
2192 warnings,
2193 vec![Warning {
2194 source: Span {
2195 data: "alt=Sunset,width=300,,height=400",
2196 line: 3,
2197 col: 12,
2198 offset: 29,
2199 },
2200 warning: WarningType::EmptyAttributeValue,
2201 }]
2202 );
2203 }
2204
2205 #[test]
2206 fn dont_stop_at_child_section() {
2207 let mut parser = Parser::default();
2208 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2209
2210 let mi = crate::blocks::SectionBlock::parse(
2211 &BlockMetadata::new("## Section Title\n\nabc\n\n### Section 2\n\ndef"),
2212 &mut parser,
2213 &mut warnings,
2214 )
2215 .unwrap();
2216
2217 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2218 assert_eq!(mi.item.raw_context().deref(), "section");
2219 assert_eq!(mi.item.resolved_context().deref(), "section");
2220 assert!(mi.item.declared_style().is_none());
2221 assert_eq!(mi.item.id().unwrap(), "_section_title");
2222 assert!(mi.item.roles().is_empty());
2223 assert!(mi.item.options().is_empty());
2224 assert!(mi.item.title_source().is_none());
2225 assert!(mi.item.title().is_none());
2226 assert!(mi.item.anchor().is_none());
2227 assert!(mi.item.anchor_reftext().is_none());
2228 assert!(mi.item.attrlist().is_none());
2229 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2230
2231 assert_eq!(
2232 mi.item,
2233 SectionBlock {
2234 level: 1,
2235 section_title: Content {
2236 original: Span {
2237 data: "Section Title",
2238 line: 1,
2239 col: 4,
2240 offset: 3,
2241 },
2242 rendered: "Section Title",
2243 },
2244 blocks: &[
2245 Block::Simple(SimpleBlock {
2246 content: Content {
2247 original: Span {
2248 data: "abc",
2249 line: 3,
2250 col: 1,
2251 offset: 18,
2252 },
2253 rendered: "abc",
2254 },
2255 source: Span {
2256 data: "abc",
2257 line: 3,
2258 col: 1,
2259 offset: 18,
2260 },
2261 style: SimpleBlockStyle::Paragraph,
2262 title_source: None,
2263 title: None,
2264 caption: None,
2265 number: None,
2266 anchor: None,
2267 anchor_reftext: None,
2268 attrlist: None,
2269 }),
2270 Block::Section(SectionBlock {
2271 level: 2,
2272 section_title: Content {
2273 original: Span {
2274 data: "Section 2",
2275 line: 5,
2276 col: 5,
2277 offset: 27,
2278 },
2279 rendered: "Section 2",
2280 },
2281 blocks: &[Block::Simple(SimpleBlock {
2282 content: Content {
2283 original: Span {
2284 data: "def",
2285 line: 7,
2286 col: 1,
2287 offset: 38,
2288 },
2289 rendered: "def",
2290 },
2291 source: Span {
2292 data: "def",
2293 line: 7,
2294 col: 1,
2295 offset: 38,
2296 },
2297 style: SimpleBlockStyle::Paragraph,
2298 title_source: None,
2299 title: None,
2300 caption: None,
2301 number: None,
2302 anchor: None,
2303 anchor_reftext: None,
2304 attrlist: None,
2305 })],
2306 source: Span {
2307 data: "### Section 2\n\ndef",
2308 line: 5,
2309 col: 1,
2310 offset: 23,
2311 },
2312 title_source: None,
2313 title: None,
2314 anchor: None,
2315 anchor_reftext: None,
2316 attrlist: None,
2317 section_type: SectionType::Normal,
2318 section_id: Some("_section_2"),
2319 caption: None,
2320 section_number: None,
2321 })
2322 ],
2323 source: Span {
2324 data: "## Section Title\n\nabc\n\n### Section 2\n\ndef",
2325 line: 1,
2326 col: 1,
2327 offset: 0,
2328 },
2329 title_source: None,
2330 title: None,
2331 anchor: None,
2332 anchor_reftext: None,
2333 attrlist: None,
2334 section_type: SectionType::Normal,
2335 section_id: Some("_section_title"),
2336 caption: None,
2337 section_number: None,
2338 }
2339 );
2340
2341 assert_eq!(
2342 mi.after,
2343 Span {
2344 data: "",
2345 line: 7,
2346 col: 4,
2347 offset: 41
2348 }
2349 );
2350 }
2351
2352 #[test]
2353 fn stop_at_peer_section() {
2354 let mut parser = Parser::default();
2355 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2356
2357 let mi = crate::blocks::SectionBlock::parse(
2358 &BlockMetadata::new("## Section Title\n\nabc\n\n## Section 2\n\ndef"),
2359 &mut parser,
2360 &mut warnings,
2361 )
2362 .unwrap();
2363
2364 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2365 assert_eq!(mi.item.raw_context().deref(), "section");
2366 assert_eq!(mi.item.resolved_context().deref(), "section");
2367 assert!(mi.item.declared_style().is_none());
2368 assert_eq!(mi.item.id().unwrap(), "_section_title");
2369 assert!(mi.item.roles().is_empty());
2370 assert!(mi.item.options().is_empty());
2371 assert!(mi.item.title_source().is_none());
2372 assert!(mi.item.title().is_none());
2373 assert!(mi.item.anchor().is_none());
2374 assert!(mi.item.anchor_reftext().is_none());
2375 assert!(mi.item.attrlist().is_none());
2376 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2377
2378 assert_eq!(
2379 mi.item,
2380 SectionBlock {
2381 level: 1,
2382 section_title: Content {
2383 original: Span {
2384 data: "Section Title",
2385 line: 1,
2386 col: 4,
2387 offset: 3,
2388 },
2389 rendered: "Section Title",
2390 },
2391 blocks: &[Block::Simple(SimpleBlock {
2392 content: Content {
2393 original: Span {
2394 data: "abc",
2395 line: 3,
2396 col: 1,
2397 offset: 18,
2398 },
2399 rendered: "abc",
2400 },
2401 source: Span {
2402 data: "abc",
2403 line: 3,
2404 col: 1,
2405 offset: 18,
2406 },
2407 style: SimpleBlockStyle::Paragraph,
2408 title_source: None,
2409 title: None,
2410 caption: None,
2411 number: None,
2412 anchor: None,
2413 anchor_reftext: None,
2414 attrlist: None,
2415 })],
2416 source: Span {
2417 data: "## Section Title\n\nabc",
2418 line: 1,
2419 col: 1,
2420 offset: 0,
2421 },
2422 title_source: None,
2423 title: None,
2424 anchor: None,
2425 anchor_reftext: None,
2426 attrlist: None,
2427 section_type: SectionType::Normal,
2428 section_id: Some("_section_title"),
2429 caption: None,
2430 section_number: None,
2431 }
2432 );
2433
2434 assert_eq!(
2435 mi.after,
2436 Span {
2437 data: "## Section 2\n\ndef",
2438 line: 5,
2439 col: 1,
2440 offset: 23
2441 }
2442 );
2443 }
2444
2445 #[test]
2446 fn stop_at_ancestor_section() {
2447 let mut parser = Parser::default();
2448 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2449
2450 let mi = crate::blocks::SectionBlock::parse(
2451 &BlockMetadata::new("### Section Title\n\nabc\n\n## Section 2\n\ndef"),
2452 &mut parser,
2453 &mut warnings,
2454 )
2455 .unwrap();
2456
2457 assert_eq!(mi.item.content_model(), ContentModel::Compound);
2458 assert_eq!(mi.item.raw_context().deref(), "section");
2459 assert_eq!(mi.item.resolved_context().deref(), "section");
2460 assert!(mi.item.declared_style().is_none());
2461 assert_eq!(mi.item.id().unwrap(), "_section_title");
2462 assert!(mi.item.roles().is_empty());
2463 assert!(mi.item.options().is_empty());
2464 assert!(mi.item.title_source().is_none());
2465 assert!(mi.item.title().is_none());
2466 assert!(mi.item.anchor().is_none());
2467 assert!(mi.item.anchor_reftext().is_none());
2468 assert!(mi.item.attrlist().is_none());
2469 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
2470
2471 assert_eq!(
2472 mi.item,
2473 SectionBlock {
2474 level: 2,
2475 section_title: Content {
2476 original: Span {
2477 data: "Section Title",
2478 line: 1,
2479 col: 5,
2480 offset: 4,
2481 },
2482 rendered: "Section Title",
2483 },
2484 blocks: &[Block::Simple(SimpleBlock {
2485 content: Content {
2486 original: Span {
2487 data: "abc",
2488 line: 3,
2489 col: 1,
2490 offset: 19,
2491 },
2492 rendered: "abc",
2493 },
2494 source: Span {
2495 data: "abc",
2496 line: 3,
2497 col: 1,
2498 offset: 19,
2499 },
2500 style: SimpleBlockStyle::Paragraph,
2501 title_source: None,
2502 title: None,
2503 caption: None,
2504 number: None,
2505 anchor: None,
2506 anchor_reftext: None,
2507 attrlist: None,
2508 })],
2509 source: Span {
2510 data: "### Section Title\n\nabc",
2511 line: 1,
2512 col: 1,
2513 offset: 0,
2514 },
2515 title_source: None,
2516 title: None,
2517 anchor: None,
2518 anchor_reftext: None,
2519 attrlist: None,
2520 section_type: SectionType::Normal,
2521 section_id: Some("_section_title"),
2522 caption: None,
2523 section_number: None,
2524 }
2525 );
2526
2527 assert_eq!(
2528 mi.after,
2529 Span {
2530 data: "## Section 2\n\ndef",
2531 line: 5,
2532 col: 1,
2533 offset: 24
2534 }
2535 );
2536 }
2537
2538 #[test]
2539 fn section_title_with_markup() {
2540 let mut parser = Parser::default();
2541 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2542
2543 let mi = crate::blocks::SectionBlock::parse(
2544 &BlockMetadata::new("## Section with *bold* text"),
2545 &mut parser,
2546 &mut warnings,
2547 )
2548 .unwrap();
2549
2550 assert_eq!(
2551 mi.item.section_title_source(),
2552 Span {
2553 data: "Section with *bold* text",
2554 line: 1,
2555 col: 4,
2556 offset: 3,
2557 }
2558 );
2559
2560 assert_eq!(
2561 mi.item.section_title(),
2562 "Section with <strong>bold</strong> text"
2563 );
2564
2565 assert_eq!(mi.item.section_type(), SectionType::Normal);
2566 assert_eq!(mi.item.id().unwrap(), "_section_with_bold_text");
2567 }
2568
2569 #[test]
2570 fn section_title_with_special_chars() {
2571 let mut parser = Parser::default();
2572 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2573
2574 let mi = crate::blocks::SectionBlock::parse(
2575 &BlockMetadata::new("## Section with <brackets> & ampersands"),
2576 &mut parser,
2577 &mut warnings,
2578 )
2579 .unwrap();
2580
2581 assert_eq!(
2582 mi.item.section_title_source(),
2583 Span {
2584 data: "Section with <brackets> & ampersands",
2585 line: 1,
2586 col: 4,
2587 offset: 3,
2588 }
2589 );
2590
2591 assert_eq!(
2592 mi.item.section_title(),
2593 "Section with <brackets> & ampersands"
2594 );
2595
2596 assert_eq!(mi.item.section_type(), SectionType::Normal);
2597 }
2598
2599 #[test]
2600 fn err_level_0_section_heading() {
2601 let mut parser = Parser::default();
2602 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2603
2604 let result = crate::blocks::SectionBlock::parse(
2605 &BlockMetadata::new("# Document Title"),
2606 &mut parser,
2607 &mut warnings,
2608 );
2609
2610 assert!(result.is_none());
2611
2612 assert_eq!(
2613 warnings,
2614 vec![Warning {
2615 source: Span {
2616 data: "# Document Title",
2617 line: 1,
2618 col: 1,
2619 offset: 0,
2620 },
2621 warning: WarningType::Level0SectionHeadingNotSupported,
2622 }]
2623 );
2624 }
2625
2626 #[test]
2627 fn err_section_heading_level_exceeds_maximum() {
2628 let mut parser = Parser::default();
2629 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2630
2631 let result = crate::blocks::SectionBlock::parse(
2632 &BlockMetadata::new("####### Level 6 Section"),
2633 &mut parser,
2634 &mut warnings,
2635 );
2636
2637 assert!(result.is_none());
2638
2639 assert_eq!(
2640 warnings,
2641 vec![Warning {
2642 source: Span {
2643 data: "####### Level 6 Section",
2644 line: 1,
2645 col: 1,
2646 offset: 0,
2647 },
2648 warning: WarningType::SectionHeadingLevelExceedsMaximum(6),
2649 }]
2650 );
2651 }
2652
2653 #[test]
2654 fn valid_maximum_level_5_section() {
2655 let mut parser = Parser::default();
2656 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2657
2658 let mi = crate::blocks::SectionBlock::parse(
2659 &BlockMetadata::new("###### Level 5 Section"),
2660 &mut parser,
2661 &mut warnings,
2662 )
2663 .unwrap();
2664
2665 assert!(warnings.is_empty());
2666
2667 assert_eq!(mi.item.level(), 5);
2668 assert_eq!(mi.item.section_title(), "Level 5 Section");
2669 assert_eq!(mi.item.section_type(), SectionType::Normal);
2670 assert_eq!(mi.item.id().unwrap(), "_level_5_section");
2671 }
2672
2673 #[test]
2674 fn warn_section_level_skipped() {
2675 let mut parser = Parser::default();
2676 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2677
2678 let mi = crate::blocks::SectionBlock::parse(
2679 &BlockMetadata::new("## Level 1\n\n#### Level 3 (skipped level 2)"),
2680 &mut parser,
2681 &mut warnings,
2682 )
2683 .unwrap();
2684
2685 assert_eq!(mi.item.level(), 1);
2686 assert_eq!(mi.item.section_title(), "Level 1");
2687 assert_eq!(mi.item.section_type(), SectionType::Normal);
2688 assert_eq!(mi.item.nested_blocks().len(), 1);
2689 assert_eq!(mi.item.id().unwrap(), "_level_1");
2690
2691 assert_eq!(
2692 warnings,
2693 vec![Warning {
2694 source: Span {
2695 data: "#### Level 3 (skipped level 2)",
2696 line: 3,
2697 col: 1,
2698 offset: 12,
2699 },
2700 warning: WarningType::SectionHeadingLevelSkipped(1, 3),
2701 }]
2702 );
2703 }
2704 }
2705
2706 #[test]
2707 fn warn_multiple_section_levels_skipped() {
2708 let mut parser = Parser::default();
2709 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2710
2711 let mi = crate::blocks::SectionBlock::parse(
2712 &BlockMetadata::new("== Level 1\n\n===== Level 4 (skipped levels 2 and 3)"),
2713 &mut parser,
2714 &mut warnings,
2715 )
2716 .unwrap();
2717
2718 assert_eq!(mi.item.level(), 1);
2719 assert_eq!(mi.item.section_title(), "Level 1");
2720 assert_eq!(mi.item.section_type(), SectionType::Normal);
2721 assert_eq!(mi.item.nested_blocks().len(), 1);
2722 assert_eq!(mi.item.id().unwrap(), "_level_1");
2723
2724 assert_eq!(
2725 warnings,
2726 vec![Warning {
2727 source: Span {
2728 data: "===== Level 4 (skipped levels 2 and 3)",
2729 line: 3,
2730 col: 1,
2731 offset: 12,
2732 },
2733 warning: WarningType::SectionHeadingLevelSkipped(1, 4),
2734 }]
2735 );
2736 }
2737
2738 #[test]
2739 fn no_warning_for_consecutive_section_levels() {
2740 let mut parser = Parser::default();
2741 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
2742
2743 let mi = crate::blocks::SectionBlock::parse(
2744 &BlockMetadata::new("== Level 1\n\n=== Level 2 (no skip)"),
2745 &mut parser,
2746 &mut warnings,
2747 )
2748 .unwrap();
2749
2750 assert_eq!(mi.item.level(), 1);
2751 assert_eq!(mi.item.section_title(), "Level 1");
2752 assert_eq!(mi.item.section_type(), SectionType::Normal);
2753 assert_eq!(mi.item.nested_blocks().len(), 1);
2754 assert_eq!(mi.item.id().unwrap(), "_level_1");
2755
2756 assert!(warnings.is_empty());
2757 }
2758
2759 #[test]
2760 fn section_id_generation_basic() {
2761 let input = "== Section One";
2762 let mut parser = Parser::default();
2763 let document = parser.parse(input);
2764
2765 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2766 assert_eq!(section.id(), Some("_section_one"));
2767 } else {
2768 panic!("Expected section block");
2769 }
2770 }
2771
2772 #[test]
2773 fn section_id_generation_with_special_characters() {
2774 let input = "== We're back! & Company";
2775 let mut parser = Parser::default();
2776 let document = parser.parse(input);
2777
2778 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2779 assert_eq!(section.id(), Some("_were_back_company"));
2780 } else {
2781 panic!("Expected section block");
2782 }
2783 }
2784
2785 #[test]
2786 fn section_id_generation_with_entities() {
2787 let input = "== Ben & Jerry "Ice Cream"";
2788 let mut parser = Parser::default();
2789 let document = parser.parse(input);
2790
2791 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2792 assert_eq!(section.id(), Some("_ben_jerry_ice_cream"));
2793 } else {
2794 panic!("Expected section block");
2795 }
2796 }
2797
2798 #[test]
2799 fn section_id_generation_disabled_when_sectids_unset() {
2800 let input = ":!sectids:\n\n== Section One";
2801 let mut parser = Parser::default();
2802 let document = parser.parse(input);
2803
2804 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2805 assert_eq!(section.id(), None);
2806 } else {
2807 panic!("Expected section block");
2808 }
2809 }
2810
2811 #[test]
2812 fn section_id_generation_with_custom_prefix() {
2813 let input = ":idprefix: id_\n\n== Section One";
2814 let mut parser = Parser::default();
2815 let document = parser.parse(input);
2816
2817 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2818 assert_eq!(section.id(), Some("id_section_one"));
2819 } else {
2820 panic!("Expected section block");
2821 }
2822 }
2823
2824 #[test]
2825 fn section_id_generation_with_custom_separator() {
2826 let input = ":idseparator: -\n\n== Section One";
2827 let mut parser = Parser::default();
2828 let document = parser.parse(input);
2829
2830 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2831 assert_eq!(section.id(), Some("_section-one"));
2832 } else {
2833 panic!("Expected section block");
2834 }
2835 }
2836
2837 #[test]
2838 fn section_id_generation_with_empty_prefix() {
2839 let input = ":idprefix:\n\n== Section One";
2840 let mut parser = Parser::default();
2841 let document = parser.parse(input);
2842
2843 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2844 assert_eq!(section.id(), Some("section_one"));
2845 } else {
2846 panic!("Expected section block");
2847 }
2848 }
2849
2850 #[test]
2851 fn section_id_generation_removes_trailing_separator() {
2852 let input = ":idseparator: -\n\n== Section Title-";
2853 let mut parser = Parser::default();
2854 let document = parser.parse(input);
2855
2856 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2857 assert_eq!(section.id(), Some("_section-title"));
2858 } else {
2859 panic!("Expected section block");
2860 }
2861 }
2862
2863 #[test]
2864 fn section_id_generation_removes_leading_separator_when_prefix_empty() {
2865 let input = ":idprefix:\n:idseparator: -\n\n== -Section Title";
2866 let mut parser = Parser::default();
2867 let document = parser.parse(input);
2868
2869 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2870 assert_eq!(section.id(), Some("section-title"));
2871 } else {
2872 panic!("Expected section block");
2873 }
2874 }
2875
2876 #[test]
2877 fn section_id_generation_handles_multiple_trailing_separators() {
2878 let input = ":idseparator: _\n\n== Title with Multiple Dots...";
2879 let mut parser = Parser::default();
2880 let document = parser.parse(input);
2881
2882 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2883 assert_eq!(section.id(), Some("_title_with_multiple_dots"));
2884 } else {
2885 panic!("Expected section block");
2886 }
2887 }
2888
2889 #[test]
2890 fn warn_duplicate_manual_section_id() {
2891 let input = "[#my_id]\n== First Section\n\n[#my_id]\n== Second Section";
2892 let mut parser = Parser::default();
2893 let document = parser.parse(input);
2894
2895 let mut warnings = document.warnings();
2896
2897 assert_eq!(
2898 warnings.next().unwrap(),
2899 Warning {
2900 source: Span {
2901 data: "[#my_id]\n== Second Section",
2902 line: 4,
2903 col: 1,
2904 offset: 27,
2905 },
2906 warning: WarningType::DuplicateId("my_id".to_owned()),
2907 }
2908 );
2909
2910 assert!(warnings.next().is_none());
2911 }
2912
2913 #[test]
2914 fn section_with_custom_reftext_attribute() {
2915 let input = "[reftext=\"Custom Reference Text\"]\n== Section Title";
2916 let mut parser = Parser::default();
2917 let document = parser.parse(input);
2918
2919 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2920 assert_eq!(section.id(), Some("_section_title"));
2921 } else {
2922 panic!("Expected section block");
2923 }
2924
2925 let catalog = document.catalog();
2926 let entry = catalog.get_ref("_section_title");
2927 assert!(entry.is_some());
2928 assert_eq!(
2929 entry.unwrap().reftext,
2930 Some("Custom Reference Text".to_string())
2931 );
2932 }
2933
2934 #[test]
2935 fn section_without_reftext_uses_title() {
2936 let input = "== Section Title";
2937 let mut parser = Parser::default();
2938 let document = parser.parse(input);
2939
2940 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
2941 assert_eq!(section.id(), Some("_section_title"));
2942 } else {
2943 panic!("Expected section block");
2944 }
2945
2946 let catalog = document.catalog();
2947 let entry = catalog.get_ref("_section_title");
2948 assert!(entry.is_some());
2949 assert_eq!(entry.unwrap().reftext, Some("Section Title".to_string()));
2950 }
2951
2952 mod section_numbering {
2953 use crate::{blocks::Block, tests::prelude::*};
2954
2955 #[test]
2956 fn single_section_with_sectnums() {
2957 let input = ":sectnums:\n\n== First Section";
2958 let mut parser = Parser::default();
2959 let document = parser.parse(input);
2960
2961 if let Some(Block::Section(section)) = document.nested_blocks().next() {
2962 let section_number = section.section_number();
2963 assert!(section_number.is_some());
2964 assert_eq!(section_number.unwrap().to_string(), "1");
2965 assert_eq!(section_number.unwrap().components(), [1]);
2966 } else {
2967 panic!("Expected section block");
2968 }
2969 }
2970
2971 #[test]
2972 fn multiple_level_1_sections() {
2973 let input = ":sectnums:\n\n== First Section\n\n== Second Section\n\n== Third Section";
2974 let mut parser = Parser::default();
2975 let document = parser.parse(input);
2976
2977 let mut sections = document.nested_blocks().filter_map(|block| {
2978 if let Block::Section(section) = block {
2979 Some(section)
2980 } else {
2981 None
2982 }
2983 });
2984
2985 let first = sections.next().unwrap();
2986 assert_eq!(first.section_number().unwrap().to_string(), "1");
2987
2988 let second = sections.next().unwrap();
2989 assert_eq!(second.section_number().unwrap().to_string(), "2");
2990
2991 let third = sections.next().unwrap();
2992 assert_eq!(third.section_number().unwrap().to_string(), "3");
2993 }
2994
2995 #[test]
2996 fn nested_sections() {
2997 let input = ":sectnums:\n\n== Level 1\n\n=== Level 2\n\n==== Level 3";
2998 let document = Parser::default().parse(input);
2999
3000 if let Some(Block::Section(level1)) = document.nested_blocks().next() {
3001 assert_eq!(level1.section_number().unwrap().to_string(), "1");
3002
3003 if let Some(Block::Section(level2)) = level1.nested_blocks().next() {
3004 assert_eq!(level2.section_number().unwrap().to_string(), "1.1");
3005
3006 if let Some(Block::Section(level3)) = level2.nested_blocks().next() {
3007 assert_eq!(level3.section_number().unwrap().to_string(), "1.1.1");
3008 } else {
3009 panic!("Expected level 3 section");
3010 }
3011 } else {
3012 panic!("Expected level 2 section");
3013 }
3014 } else {
3015 panic!("Expected level 1 section");
3016 }
3017 }
3018
3019 #[test]
3020 fn mixed_section_levels() {
3021 let input = ":sectnums:\n\n== First\n\n=== First.One\n\n=== First.Two\n\n== Second\n\n=== Second.One";
3022 let document = Parser::default().parse(input);
3023
3024 let mut sections = document.nested_blocks().filter_map(|block| {
3025 if let Block::Section(section) = block {
3026 Some(section)
3027 } else {
3028 None
3029 }
3030 });
3031
3032 let first = sections.next().unwrap();
3033 assert_eq!(first.section_number().unwrap().to_string(), "1");
3034
3035 let first_one = first
3036 .nested_blocks()
3037 .filter_map(|block| {
3038 if let Block::Section(section) = block {
3039 Some(section)
3040 } else {
3041 None
3042 }
3043 })
3044 .next()
3045 .unwrap();
3046 assert_eq!(first_one.section_number().unwrap().to_string(), "1.1");
3047
3048 let first_two = first
3049 .nested_blocks()
3050 .filter_map(|block| {
3051 if let Block::Section(section) = block {
3052 Some(section)
3053 } else {
3054 None
3055 }
3056 })
3057 .nth(1)
3058 .unwrap();
3059 assert_eq!(first_two.section_number().unwrap().to_string(), "1.2");
3060
3061 let second = sections.next().unwrap();
3062 assert_eq!(second.section_number().unwrap().to_string(), "2");
3063
3064 let second_one = second
3065 .nested_blocks()
3066 .filter_map(|block| {
3067 if let Block::Section(section) = block {
3068 Some(section)
3069 } else {
3070 None
3071 }
3072 })
3073 .next()
3074 .unwrap();
3075 assert_eq!(second_one.section_number().unwrap().to_string(), "2.1");
3076 }
3077
3078 #[test]
3079 fn sectnums_disabled() {
3080 let input = "== First Section\n\n== Second Section";
3081 let mut parser = Parser::default();
3082 let document = parser.parse(input);
3083
3084 for block in document.nested_blocks() {
3085 if let Block::Section(section) = block {
3086 assert!(section.section_number().is_none());
3087 }
3088 }
3089 }
3090
3091 #[test]
3092 fn sectnums_explicitly_unset() {
3093 let input = ":!sectnums:\n\n== First Section\n\n== Second Section";
3094 let mut parser = Parser::default();
3095 let document = parser.parse(input);
3096
3097 for block in document.nested_blocks() {
3098 if let Block::Section(section) = block {
3099 assert!(section.section_number().is_none());
3100 }
3101 }
3102 }
3103
3104 #[test]
3105 fn deep_nesting() {
3106 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";
3107 let document = Parser::default().parse(input);
3108
3109 if let Some(Block::Section(l1)) = document.nested_blocks().next() {
3110 assert_eq!(l1.section_number().unwrap().to_string(), "1");
3111
3112 if let Some(Block::Section(l2)) = l1.nested_blocks().next() {
3113 assert_eq!(l2.section_number().unwrap().to_string(), "1.1");
3114
3115 if let Some(Block::Section(l3)) = l2.nested_blocks().next() {
3116 assert_eq!(l3.section_number().unwrap().to_string(), "1.1.1");
3117
3118 if let Some(Block::Section(l4)) = l3.nested_blocks().next() {
3119 assert_eq!(l4.section_number().unwrap().to_string(), "1.1.1.1");
3120
3121 if let Some(Block::Section(l5)) = l4.nested_blocks().next() {
3122 assert_eq!(l5.section_number().unwrap().to_string(), "1.1.1.1.1");
3123 } else {
3124 panic!("Expected level 5 section");
3125 }
3126 } else {
3127 panic!("Expected level 4 section");
3128 }
3129 } else {
3130 panic!("Expected level 3 section");
3131 }
3132 } else {
3133 panic!("Expected level 2 section");
3134 }
3135 } else {
3136 panic!("Expected level 1 section");
3137 }
3138 }
3139 }
3140
3141 #[test]
3142 fn impl_debug() {
3143 let mut parser = Parser::default();
3144 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3145
3146 let section = crate::blocks::SectionBlock::parse(
3147 &BlockMetadata::new("== Section Title"),
3148 &mut parser,
3149 &mut warnings,
3150 )
3151 .unwrap()
3152 .item;
3153
3154 assert_eq!(
3155 format!("{section:#?}"),
3156 r#"SectionBlock {
3157 level: 1,
3158 section_title: Content {
3159 original: Span {
3160 data: "Section Title",
3161 line: 1,
3162 col: 4,
3163 offset: 3,
3164 },
3165 rendered: "Section Title",
3166 },
3167 blocks: &[],
3168 source: Span {
3169 data: "== Section Title",
3170 line: 1,
3171 col: 1,
3172 offset: 0,
3173 },
3174 title_source: None,
3175 title: None,
3176 anchor: None,
3177 anchor_reftext: None,
3178 attrlist: None,
3179 section_type: SectionType::Normal,
3180 section_id: Some(
3181 "_section_title",
3182 ),
3183 caption: None,
3184 section_number: None,
3185}"#
3186 );
3187 }
3188
3189 mod section_type {
3190 use crate::blocks::section::SectionType;
3191
3192 #[test]
3193 fn impl_debug() {
3194 let st = SectionType::Normal;
3195 assert_eq!(format!("{st:?}"), "SectionType::Normal");
3196
3197 let st = SectionType::Appendix;
3198 assert_eq!(format!("{st:?}"), "SectionType::Appendix");
3199
3200 let st = SectionType::Discrete;
3201 assert_eq!(format!("{st:?}"), "SectionType::Discrete");
3202 }
3203 }
3204
3205 mod section_number {
3206 mod assign_next_number {
3207 use crate::blocks::section::SectionNumber;
3208
3209 #[test]
3210 fn default() {
3211 let sn = SectionNumber::default();
3212 assert_eq!(sn.components(), []);
3213 assert_eq!(sn.to_string(), "");
3214 assert_eq!(
3215 format!("{sn:?}"),
3216 "SectionNumber { section_type: SectionType::Normal, components: &[] }"
3217 );
3218 }
3219
3220 #[test]
3221 fn level_1() {
3222 let mut sn = SectionNumber::default();
3223 sn.assign_next_number(1);
3224 assert_eq!(sn.components(), [1]);
3225 assert_eq!(sn.to_string(), "1");
3226 assert_eq!(
3227 format!("{sn:?}"),
3228 "SectionNumber { section_type: SectionType::Normal, components: &[1] }"
3229 );
3230 }
3231
3232 #[test]
3233 fn level_3() {
3234 let mut sn = SectionNumber::default();
3235 sn.assign_next_number(3);
3236 assert_eq!(sn.components(), [1, 1, 1]);
3237 assert_eq!(sn.to_string(), "1.1.1");
3238 assert_eq!(
3239 format!("{sn:?}"),
3240 "SectionNumber { section_type: SectionType::Normal, components: &[1, 1, 1] }"
3241 );
3242 }
3243
3244 #[test]
3245 fn level_3_then_1() {
3246 let mut sn = SectionNumber::default();
3247 sn.assign_next_number(3);
3248 sn.assign_next_number(1);
3249 assert_eq!(sn.components(), [2]);
3250 assert_eq!(sn.to_string(), "2");
3251 assert_eq!(
3252 format!("{sn:?}"),
3253 "SectionNumber { section_type: SectionType::Normal, components: &[2] }"
3254 );
3255 }
3256
3257 #[test]
3258 fn level_3_then_1_then_2() {
3259 let mut sn = SectionNumber::default();
3260 sn.assign_next_number(3);
3261 sn.assign_next_number(1);
3262 sn.assign_next_number(2);
3263 assert_eq!(sn.components(), [2, 1]);
3264 assert_eq!(sn.to_string(), "2.1");
3265 assert_eq!(
3266 format!("{sn:?}"),
3267 "SectionNumber { section_type: SectionType::Normal, components: &[2, 1] }"
3268 );
3269 }
3270 }
3271
3272 mod assign_next_number_appendix {
3273 use crate::blocks::{SectionType, section::SectionNumber};
3274
3275 #[test]
3276 fn default() {
3277 let sn = SectionNumber {
3278 section_type: SectionType::Appendix,
3279 components: vec![],
3280 };
3281 assert_eq!(sn.components(), []);
3282 assert_eq!(sn.to_string(), "");
3283 assert_eq!(
3284 format!("{sn:?}"),
3285 "SectionNumber { section_type: SectionType::Appendix, components: &[] }"
3286 );
3287 }
3288
3289 #[test]
3290 fn level_1() {
3291 let mut sn = SectionNumber {
3292 section_type: SectionType::Appendix,
3293 components: vec![],
3294 };
3295 sn.assign_next_number(1);
3296 assert_eq!(sn.components(), [1]);
3297 assert_eq!(sn.to_string(), "A");
3298 assert_eq!(
3299 format!("{sn:?}"),
3300 "SectionNumber { section_type: SectionType::Appendix, components: &[1] }"
3301 );
3302 }
3303
3304 #[test]
3305 fn level_3() {
3306 let mut sn = SectionNumber {
3307 section_type: SectionType::Appendix,
3308 components: vec![],
3309 };
3310 sn.assign_next_number(3);
3311 assert_eq!(sn.components(), [1, 1, 1]);
3312 assert_eq!(sn.to_string(), "A.1.1");
3313 assert_eq!(
3314 format!("{sn:?}"),
3315 "SectionNumber { section_type: SectionType::Appendix, components: &[1, 1, 1] }"
3316 );
3317 }
3318
3319 #[test]
3320 fn level_3_then_1() {
3321 let mut sn = SectionNumber {
3322 section_type: SectionType::Appendix,
3323 components: vec![],
3324 };
3325 sn.assign_next_number(3);
3326 sn.assign_next_number(1);
3327 assert_eq!(sn.components(), [2]);
3328 assert_eq!(sn.to_string(), "B");
3329 assert_eq!(
3330 format!("{sn:?}"),
3331 "SectionNumber { section_type: SectionType::Appendix, components: &[2] }"
3332 );
3333 }
3334
3335 #[test]
3336 fn level_3_then_1_then_2() {
3337 let mut sn = SectionNumber {
3338 section_type: SectionType::Appendix,
3339 components: vec![],
3340 };
3341 sn.assign_next_number(3);
3342 sn.assign_next_number(1);
3343 sn.assign_next_number(2);
3344 assert_eq!(sn.components(), [2, 1]);
3345 assert_eq!(sn.to_string(), "B.1");
3346 assert_eq!(
3347 format!("{sn:?}"),
3348 "SectionNumber { section_type: SectionType::Appendix, components: &[2, 1] }"
3349 );
3350 }
3351 }
3352 }
3353
3354 mod discrete_headings {
3355 use std::ops::Deref;
3356
3357 use crate::{
3358 blocks::{ContentModel, metadata::BlockMetadata, section::SectionType},
3359 tests::prelude::*,
3360 };
3361
3362 #[test]
3363 fn basic_case() {
3364 let mut parser = Parser::default();
3365 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3366
3367 let mi = crate::blocks::SectionBlock::parse(
3368 &BlockMetadata::new("[discrete]\n== Discrete Heading"),
3369 &mut parser,
3370 &mut warnings,
3371 )
3372 .unwrap();
3373
3374 assert_eq!(mi.item.content_model(), ContentModel::Compound);
3375 assert_eq!(mi.item.raw_context().deref(), "section");
3376 assert_eq!(mi.item.level(), 1);
3377 assert_eq!(mi.item.section_title(), "Discrete Heading");
3378 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3379 assert!(mi.item.nested_blocks().next().is_none());
3380 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
3381 assert!(mi.item.title().is_none());
3382 assert!(mi.item.anchor().is_none());
3383 assert!(mi.item.attrlist().is_some());
3384 assert_eq!(mi.item.section_number(), None);
3385 assert!(warnings.is_empty());
3386
3387 assert_eq!(
3388 mi.item.section_title_source(),
3389 Span {
3390 data: "Discrete Heading",
3391 line: 2,
3392 col: 4,
3393 offset: 14,
3394 }
3395 );
3396
3397 assert_eq!(
3398 mi.item.span(),
3399 Span {
3400 data: "[discrete]\n== Discrete Heading",
3401 line: 1,
3402 col: 1,
3403 offset: 0,
3404 }
3405 );
3406
3407 assert_eq!(
3408 mi.after,
3409 Span {
3410 data: "",
3411 line: 2,
3412 col: 20,
3413 offset: 30,
3414 }
3415 );
3416 }
3417
3418 #[test]
3419 fn float_style() {
3420 let mut parser = Parser::default();
3421 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3422
3423 let mi = crate::blocks::SectionBlock::parse(
3424 &BlockMetadata::new("[float]\n== Floating Heading"),
3425 &mut parser,
3426 &mut warnings,
3427 )
3428 .unwrap();
3429
3430 assert_eq!(mi.item.level(), 1);
3431 assert_eq!(mi.item.section_title(), "Floating Heading");
3432 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3433 assert!(mi.item.nested_blocks().next().is_none());
3434 assert!(warnings.is_empty());
3435 }
3436
3437 #[test]
3438 fn has_no_child_blocks() {
3439 let mut parser = Parser::default();
3440 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3441
3442 let mi = crate::blocks::SectionBlock::parse(
3443 &BlockMetadata::new("[discrete]\n== Discrete Heading\n\nThis is a paragraph."),
3444 &mut parser,
3445 &mut warnings,
3446 )
3447 .unwrap();
3448
3449 assert_eq!(mi.item.level(), 1);
3450 assert_eq!(mi.item.section_title(), "Discrete Heading");
3451 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3452
3453 assert!(mi.item.nested_blocks().next().is_none());
3455
3456 assert_eq!(
3458 mi.after,
3459 Span {
3460 data: "This is a paragraph.",
3461 line: 4,
3462 col: 1,
3463 offset: 32,
3464 }
3465 );
3466
3467 assert!(warnings.is_empty());
3468 }
3469
3470 #[test]
3471 fn not_in_section_hierarchy() {
3472 let input = "== Section 1\n\n[discrete]\n=== Discrete\n\n=== Section 1.1";
3473 let mut parser = Parser::default();
3474 let document = parser.parse(input);
3475
3476 let mut blocks = document.nested_blocks();
3477
3478 if let Some(crate::blocks::Block::Section(section)) = blocks.next() {
3480 assert_eq!(section.section_title(), "Section 1");
3481 assert_eq!(section.level(), 1);
3482 assert_eq!(section.section_type(), SectionType::Normal);
3483
3484 let mut children = section.nested_blocks();
3485
3486 if let Some(crate::blocks::Block::Section(discrete)) = children.next() {
3488 assert_eq!(discrete.section_title(), "Discrete");
3489 assert_eq!(discrete.level(), 2);
3490 assert_eq!(discrete.section_type(), SectionType::Discrete);
3491 assert!(discrete.nested_blocks().next().is_none());
3492 } else {
3493 panic!("Expected discrete heading block");
3494 }
3495
3496 if let Some(crate::blocks::Block::Section(subsection)) = children.next() {
3498 assert_eq!(subsection.section_title(), "Section 1.1");
3499 assert_eq!(subsection.level(), 2);
3500 assert_eq!(subsection.section_type(), SectionType::Normal);
3501 } else {
3502 panic!("Expected subsection block");
3503 }
3504 } else {
3505 panic!("Expected section block");
3506 }
3507 }
3508
3509 #[test]
3510 fn has_auto_id() {
3511 let input = "[discrete]\n== Discrete Heading";
3512 let mut parser = Parser::default();
3513 let document = parser.parse(input);
3514
3515 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
3516 assert_eq!(section.id(), Some("_discrete_heading"));
3518 } else {
3519 panic!("Expected section block");
3520 }
3521 }
3522
3523 #[test]
3524 fn with_manual_id() {
3525 let input = "[discrete#my-id]\n== Discrete Heading";
3526 let mut parser = Parser::default();
3527 let document = parser.parse(input);
3528
3529 if let Some(crate::blocks::Block::Section(section)) = document.nested_blocks().next() {
3530 assert_eq!(section.id(), Some("my-id"));
3532 } else {
3533 panic!("Expected section block");
3534 }
3535 }
3536
3537 #[test]
3538 fn no_section_number() {
3539 let input = ":sectnums:\n\n== Section 1\n\n[discrete]\n=== Discrete\n\n=== Section 1.1";
3540 let mut parser = Parser::default();
3541 let document = parser.parse(input);
3542
3543 let mut blocks = document.nested_blocks();
3544
3545 if let Some(crate::blocks::Block::Section(section)) = blocks.next() {
3546 assert_eq!(section.section_title(), "Section 1");
3547 assert!(section.section_number().is_some());
3548
3549 let mut children = section.nested_blocks();
3550
3551 if let Some(crate::blocks::Block::Section(discrete)) = children.next() {
3553 assert_eq!(discrete.section_title(), "Discrete");
3554 assert_eq!(discrete.section_number(), None);
3555 } else {
3556 panic!("Expected discrete heading block");
3557 }
3558
3559 if let Some(crate::blocks::Block::Section(subsection)) = children.next() {
3561 assert_eq!(subsection.section_title(), "Section 1.1");
3562 assert!(subsection.section_number().is_some());
3563 } else {
3564 panic!("Expected subsection block");
3565 }
3566 } else {
3567 panic!("Expected section block");
3568 }
3569 }
3570
3571 #[test]
3572 fn title_can_have_markup() {
3573 let mut parser = Parser::default();
3574 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3575
3576 let mi = crate::blocks::SectionBlock::parse(
3577 &BlockMetadata::new("[discrete]\n== Discrete with *bold* text"),
3578 &mut parser,
3579 &mut warnings,
3580 )
3581 .unwrap();
3582
3583 assert_eq!(
3584 mi.item.section_title(),
3585 "Discrete with <strong>bold</strong> text"
3586 );
3587 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3588 assert!(warnings.is_empty());
3589 }
3590
3591 #[test]
3592 fn level_2() {
3593 let mut parser = Parser::default();
3594 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3595
3596 let mi = crate::blocks::SectionBlock::parse(
3597 &BlockMetadata::new("[discrete]\n=== Level 2 Discrete"),
3598 &mut parser,
3599 &mut warnings,
3600 )
3601 .unwrap();
3602
3603 assert_eq!(mi.item.level(), 2);
3604 assert_eq!(mi.item.section_title(), "Level 2 Discrete");
3605 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3606 assert!(warnings.is_empty());
3607 }
3608
3609 #[test]
3610 fn level_5() {
3611 let mut parser = Parser::default();
3612 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3613
3614 let mi = crate::blocks::SectionBlock::parse(
3615 &BlockMetadata::new("[discrete]\n====== Level 5 Discrete"),
3616 &mut parser,
3617 &mut warnings,
3618 )
3619 .unwrap();
3620
3621 assert_eq!(mi.item.level(), 5);
3622 assert_eq!(mi.item.section_title(), "Level 5 Discrete");
3623 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3624 assert!(warnings.is_empty());
3625 }
3626
3627 #[test]
3628 fn markdown_style() {
3629 let mut parser = Parser::default();
3630 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3631
3632 let mi = crate::blocks::SectionBlock::parse(
3633 &BlockMetadata::new("[discrete]\n## Discrete Heading"),
3634 &mut parser,
3635 &mut warnings,
3636 )
3637 .unwrap();
3638
3639 assert_eq!(mi.item.level(), 1);
3640 assert_eq!(mi.item.section_title(), "Discrete Heading");
3641 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3642 assert!(warnings.is_empty());
3643 }
3644
3645 #[test]
3646 fn with_block_title() {
3647 let mut parser = Parser::default();
3648 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3649
3650 let mi = crate::blocks::SectionBlock::parse(
3651 &BlockMetadata::new(".Block Title\n[discrete]\n== Discrete Heading"),
3652 &mut parser,
3653 &mut warnings,
3654 )
3655 .unwrap();
3656
3657 assert_eq!(mi.item.level(), 1);
3658 assert_eq!(mi.item.section_title(), "Discrete Heading");
3659 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3660 assert_eq!(mi.item.title(), Some("Block Title"));
3661 assert!(warnings.is_empty());
3662 }
3663
3664 #[test]
3665 fn with_anchor() {
3666 let mut parser = Parser::default();
3667 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3668
3669 let mi = crate::blocks::SectionBlock::parse(
3670 &BlockMetadata::new("[[my_anchor]]\n[discrete]\n== Discrete Heading"),
3671 &mut parser,
3672 &mut warnings,
3673 )
3674 .unwrap();
3675
3676 assert_eq!(mi.item.level(), 1);
3677 assert_eq!(mi.item.section_title(), "Discrete Heading");
3678 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3679 assert_eq!(mi.item.id(), Some("my_anchor"));
3680 assert!(warnings.is_empty());
3681 }
3682
3683 #[test]
3684 fn doesnt_include_subsequent_blocks() {
3685 let mut parser = Parser::default();
3686 let mut warnings: Vec<crate::warnings::Warning<'_>> = vec![];
3687
3688 let mi = crate::blocks::SectionBlock::parse(
3689 &BlockMetadata::new(
3690 "[discrete]\n== Discrete Heading\n\nparagraph\n\n== Next Section",
3691 ),
3692 &mut parser,
3693 &mut warnings,
3694 )
3695 .unwrap();
3696
3697 assert_eq!(mi.item.level(), 1);
3698 assert_eq!(mi.item.section_title(), "Discrete Heading");
3699 assert_eq!(mi.item.section_type(), SectionType::Discrete);
3700
3701 assert!(mi.item.nested_blocks().next().is_none());
3703
3704 assert!(mi.after.data().contains("paragraph"));
3706 assert!(mi.after.data().contains("== Next Section"));
3707
3708 assert!(warnings.is_empty());
3709 }
3710 }
3711}