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