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