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