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