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