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