asciidoc_parser/blocks/
section.rs

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/// Sections partition the document into a content hierarchy. A section is an
20/// implicit enclosure. Each section begins with a title and ends at the next
21/// sibling section, ancestor section, or end of document. Nested section levels
22/// must be sequential.
23#[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        // Take a snapshot of `sectids` value before reading child blocks because
49        // the value might be altered while parsing.
50        let sectids = parser.is_attribute_set("sectids");
51
52        let level = level_and_title.item.0;
53
54        // Assign the section type. At level 1, we look for an `appendix` section style;
55        // at all other levels, we inherit the section type from parent.
56        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        // Assign section number BEFORE parsing child blocks so that sections are
72        // numbered in document order (parent before children).
73        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        // Restore "normal" top-level section type if exiting a level 1 appendix.
134        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    /// Return the section's level.
160    ///
161    /// The section title must be prefixed with a section marker, which
162    /// indicates the section level. The number of equal signs in the marker
163    /// represents the section level using a 0-based index (e.g., two equal
164    /// signs represents level 1). A section marker can range from two to six
165    /// equal signs and must be followed by a space.
166    ///
167    /// This function will return an integer between 1 and 5.
168    pub fn level(&self) -> usize {
169        self.level
170    }
171
172    /// Return a [`Span`] containing the section title source.
173    pub fn section_title_source(&self) -> Span<'src> {
174        self.section_title.original()
175    }
176
177    /// Return the processed section title after substitutions have been
178    /// applied.
179    pub fn section_title(&'src self) -> &'src str {
180        self.section_title.rendered()
181    }
182
183    /// Return the type of this section (normal or appendix).
184    pub fn section_type(&'src self) -> SectionType {
185        self.section_type
186    }
187
188    /// Accessor intended to be used for testing only. Use the `id()` accessor
189    /// in the `IsBlock` trait to retrieve the effective ID for this block,
190    /// which considers both auto-generated IDs and manually-set IDs.
191    #[cfg(test)]
192    pub(crate) fn section_id(&'src self) -> Option<&'src str> {
193        self.section_id.as_deref()
194    }
195
196    /// Return the section number assigned to this section, if any.
197    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        // First try the default implementation (explicit IDs from anchor or attrlist)
237        self.anchor()
238            .map(|a| a.data())
239            .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id()))
240            // Fall back to auto-generated ID if no explicit ID is set
241            .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    // Skip over any block metadata (title, anchor, attrlist) to find the actual
324    // section line. We create a temporary parser to avoid modifying the real
325    // parser state.
326    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
349/// Propose a section ID from the section title.
350///
351/// This function is called when (1) no `id` attribute is specified explicitly,
352/// and (2) the `sectids` document attribute is set.
353///
354/// The ID is generated as described in the AsciiDoc language definition in [How
355/// a section ID is computed].
356///
357/// [How a section ID is computed](https://docs.asciidoctor.org/asciidoc/latest/sections/auto-ids/)
358fn 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"<[^>]+>|&lt;[^&]*&gt;|&(?:[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    // Take only first character of separator if multiple provided.
386    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/// Represents the type of a section.
412///
413/// This crate currently supports the `appendix` section style, which results in
414/// special section numbering. All other sections are treated as `Normal`
415/// sections.
416#[derive(Clone, Copy, Default, Eq, PartialEq)]
417pub enum SectionType {
418    /// Most sections are of this type.
419    #[default]
420    Normal,
421
422    /// Represents a section with the style `appendix`.
423    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/// Represents an assigned section number.
436///
437/// Section numbers aren't assigned by default, but can be enabled using the
438/// `sectnums` and `sectnumlevels` attributes as described in [Section Numbers].
439///
440/// [Section Numbers]: https://docs.asciidoctor.org/asciidoc/latest/sections/numbers/
441#[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    /// Generate the next section number for the specified level, based on this
449    /// section number.
450    ///
451    /// `level` should be between 1 and 5, though this is not enforced.
452    pub(crate) fn assign_next_number(&mut self, level: usize) {
453        // Drop any ID components beyond the desired level.
454        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    /// Iterate over the components of the section number.
464    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        // Silly test to mark the #[derive(...)] line as covered.
517        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 &lt;brackets&gt; &amp; 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 &lt;brackets&gt; &amp; 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 &amp; Jerry &#34;Ice Cream&#34;";
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}