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