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