Skip to main content

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