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