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