Skip to main content

asciidoc_parser/blocks/
section.rs

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