Skip to main content

asciidoc_parser/blocks/
block.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{
5        AdmonitionBlock, Break, CompoundDelimitedBlock, ContentModel, IsBlock, ListBlock, ListItem,
6        ListItemMarker, MediaBlock, Preamble, QuoteBlock, RawDelimitedBlock, SectionBlock,
7        SimpleBlock, TableBlock, TocBlock, media::TargetResolution, metadata::BlockMetadata,
8        starts_with_admonition_label,
9    },
10    content::{Content, SubstitutionGroup, substitute_attributes_in_reftext},
11    document::{Attribute, InterpretedValue, RefType},
12    parser::{InlineSubstitutionRenderer, ReferenceResolver, ReferenceWarnings, XrefSignifier},
13    span::MatchedItem,
14    strings::CowStr,
15    warnings::{MatchAndWarnings, Warning, WarningType},
16};
17
18/// **Block elements** form the main structure of an AsciiDoc document, starting
19/// with the document itself.
20///
21/// A block element (aka **block**) is a discrete, line-oriented chunk of
22/// content in an AsciiDoc document. Once parsed, that chunk of content becomes
23/// a block element in the parsed document model. Certain blocks may contain
24/// other blocks, so we say that blocks can be nested. The converter visits each
25/// block in turn, in document order, converting it to a corresponding chunk of
26/// output.
27///
28/// This enum represents all of the block types that are understood directly by
29/// this parser and also implements the [`IsBlock`] trait.
30#[derive(Clone, Eq, Hash, PartialEq)]
31#[non_exhaustive]
32pub enum Block<'src> {
33    /// A block that’s treated as contiguous lines of paragraph text (and
34    /// subject to normal substitutions) (e.g., a paragraph block).
35    Simple(SimpleBlock<'src>),
36
37    /// A media block is used to represent an image, video, or audio block
38    /// macro.
39    Media(MediaBlock<'src>),
40
41    /// A section helps to partition the document into a content hierarchy.
42    /// May also be a part, chapter, or special section.
43    Section(SectionBlock<'src>),
44
45    /// A list contains a sequence of items prefixed with symbol, such as a disc
46    /// (aka bullet). Each individual item in the list is represented by a
47    /// [`ListItem`].
48    List(ListBlock<'src>),
49
50    /// A list item is a special kind of block that is a member of a
51    /// [`ListBlock`] and contains one or more blocks attached to it.
52    ListItem(ListItem<'src>),
53
54    /// A delimited block that contains verbatim, raw, or comment text. The
55    /// content between the matching delimiters is not parsed for block
56    /// syntax.
57    RawDelimited(RawDelimitedBlock<'src>),
58
59    /// A delimited block that can contain other blocks.
60    CompoundDelimited(CompoundDelimitedBlock<'src>),
61
62    /// An admonition draws attention to a statement by taking it out of the
63    /// content's flow and labeling it with a priority (e.g., a note or a
64    /// warning).
65    Admonition(AdmonitionBlock<'src>),
66
67    /// A blockquote: a quote, prose excerpt, or verse, optionally attributed to
68    /// a person and a source citation.
69    Quote(QuoteBlock<'src>),
70
71    /// A table block arranges content into a grid of rows and columns.
72    Table(TableBlock<'src>),
73
74    /// Content between the end of the document header and the first section
75    /// title in the document body is called the preamble.
76    Preamble(Preamble<'src>),
77
78    /// A thematic or page break.
79    Break(Break<'src>),
80
81    /// The `toc::[]` block macro, marking where a table of contents should be
82    /// rendered under `toc-placement: macro`.
83    Toc(TocBlock<'src>),
84
85    /// When an attribute is defined in the document body using an attribute
86    /// entry, that’s simply referred to as a document attribute.
87    DocumentAttribute(Attribute<'src>),
88}
89
90impl<'src> std::fmt::Debug for Block<'src> {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Block::Simple(block) => f.debug_tuple("Block::Simple").field(block).finish(),
94            Block::Media(block) => f.debug_tuple("Block::Media").field(block).finish(),
95            Block::Section(block) => f.debug_tuple("Block::Section").field(block).finish(),
96            Block::List(block) => f.debug_tuple("Block::List").field(block).finish(),
97            Block::ListItem(block) => f.debug_tuple("Block::ListItem").field(block).finish(),
98
99            Block::RawDelimited(block) => {
100                f.debug_tuple("Block::RawDelimited").field(block).finish()
101            }
102
103            Block::CompoundDelimited(block) => f
104                .debug_tuple("Block::CompoundDelimited")
105                .field(block)
106                .finish(),
107
108            Block::Admonition(block) => f.debug_tuple("Block::Admonition").field(block).finish(),
109            Block::Quote(block) => f.debug_tuple("Block::Quote").field(block).finish(),
110            Block::Table(block) => f.debug_tuple("Block::Table").field(block).finish(),
111            Block::Preamble(block) => f.debug_tuple("Block::Preamble").field(block).finish(),
112            Block::Break(break_) => f.debug_tuple("Block::Break").field(break_).finish(),
113            Block::Toc(block) => f.debug_tuple("Block::Toc").field(block).finish(),
114
115            Block::DocumentAttribute(block) => f
116                .debug_tuple("Block::DocumentAttribute")
117                .field(block)
118                .finish(),
119        }
120    }
121}
122
123/// Outcome of attempting to parse a single [`Block`].
124///
125/// Most blocks parse to [`Parsed`](Self::Parsed). [`Dropped`](Self::Dropped)
126/// supports `attribute-missing=drop-line`: when a block-macro target references
127/// a missing attribute, Asciidoctor discards the whole block, which the parser
128/// must distinguish both from a successful parse and from "no block matched"
129/// (so the block-collection loops advance past the dropped source rather than
130/// spinning or mis-parsing it).
131// `Parsed` embeds a `Block`, which is itself a large enum (see the matching
132// allow on `Block`). This outcome is short-lived and returned by value on the
133// hot parse path, so boxing it would just trade the size for an allocation.
134#[allow(clippy::large_enum_variant)]
135pub(crate) enum BlockParseOutcome<'src> {
136    /// A block was parsed.
137    Parsed(MatchedItem<'src, Block<'src>>),
138
139    /// The input was recognized as a block macro but dropped at parse time
140    /// because its target referenced a missing attribute under
141    /// `attribute-missing=drop-line`. The contained span is where parsing
142    /// should resume (the dropped block's `after`).
143    Dropped(Span<'src>),
144
145    /// No block matched. This happens only for empty or all-blank input.
146    NoMatch,
147}
148
149impl<'src> Block<'src> {
150    /// Parse a block of any type and return a `Block` that describes it.
151    ///
152    /// Consumes any blank lines before and after the block.
153    ///
154    /// This is a test-only convenience wrapper over
155    /// [`parse_with_outcome`](Self::parse_with_outcome) that flattens the
156    /// drop-line outcome to an `Option`; production code uses
157    /// `parse_with_outcome` so it can react to a dropped block.
158    #[cfg(test)]
159    pub(crate) fn parse(
160        source: Span<'src>,
161        parser: &mut Parser,
162    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
163        let MatchAndWarnings { item, warnings } = Self::parse_internal(source, parser, None, false);
164
165        MatchAndWarnings {
166            item: match item {
167                BlockParseOutcome::Parsed(mi) => Some(mi),
168                BlockParseOutcome::Dropped(_) | BlockParseOutcome::NoMatch => None,
169            },
170            warnings,
171        }
172    }
173
174    /// Parse a block of any type, returning the full [`BlockParseOutcome`] so a
175    /// block-collection loop can advance past a block that was dropped at parse
176    /// time (`attribute-missing=drop-line`). Consumes any blank lines before
177    /// and after the block.
178    ///
179    /// This is the entry point used by production block-collection loops.
180    pub(crate) fn parse_with_outcome(
181        source: Span<'src>,
182        parser: &mut Parser,
183    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
184        Self::parse_internal(source, parser, None, false)
185    }
186
187    /// Parse a block of any type and return a `Block` that describes it.
188    ///
189    /// Will terminate early when parsing certain block types within a list
190    /// context.
191    ///
192    /// Consumes any blank lines before and after the block.
193    ///
194    /// If `is_continuation` is true, this content was attached via a `+`
195    /// continuation marker and literal blocks should preserve their
196    /// indentation.
197    pub(crate) fn parse_for_list_item(
198        source: Span<'src>,
199        parser: &mut Parser,
200        parent_list_markers: &[ListItemMarker<'src>],
201        is_continuation: bool,
202    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
203        Self::parse_internal(source, parser, Some(parent_list_markers), is_continuation)
204    }
205
206    /// Shared parser for [`parse_with_outcome`](Self::parse_with_outcome) and
207    /// [`parse_for_list_item`](Self::parse_for_list_item).
208    fn parse_internal(
209        source: Span<'src>,
210        parser: &mut Parser,
211        parent_list_markers: Option<&[ListItemMarker<'src>]>,
212        is_continuation: bool,
213    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
214        // Optimization: If the first line doesn't match any of the early indications
215        // for delimited blocks, titles, or attrlists, we can skip directly to treating
216        // this as a simple block. That saves quite a bit of parsing time.
217        let first_line = source.take_line().item.discard_whitespace();
218
219        // If it does contain any of those markers, we fall through to the more costly
220        // tests below which can more accurately classify the upcoming block.
221        if let Some(first_char) = first_line.chars().next()
222            && !matches!(
223                first_char,
224                '.' | '#'
225                    | '='
226                    | '/'
227                    | '-'
228                    | '+'
229                    | '*'
230                    | '_'
231                    | '`'
232                    | '['
233                    | ':'
234                    | '\''
235                    | '<'
236                    | '>'
237                    | '"'
238                    | '•'
239            )
240            && !first_line.contains("::")
241            && !first_line.contains(";;")
242            && !TableBlock::is_table_delimiter(&first_line)
243            && !ListItemMarker::starts_with_marker(first_line)
244            && !starts_with_admonition_label(first_line)
245            && parent_list_markers.is_none()
246            && parser.pending_block_title.is_none()
247            && let Some(MatchedItem {
248                item: simple_block,
249                after,
250            }) = SimpleBlock::parse_fast(source, parser)
251        {
252            let mut warnings = vec![];
253            let block = Self::Simple(simple_block);
254
255            // This fast path only handles a metadata-free simple block, so there
256            // is no `[[id,reftext]]` anchor reftext to resolve.
257            Self::register_block_id(
258                block.id(),
259                Self::block_reftext(&block, None).as_deref(),
260                Self::block_signifier(&block, parser),
261                block.span(),
262                parser,
263                &mut warnings,
264            );
265
266            return MatchAndWarnings {
267                item: BlockParseOutcome::Parsed(MatchedItem { item: block, after }),
268                warnings,
269            };
270        }
271
272        // Look for document attributes first since these don't support block metadata.
273        if first_line.starts_with(':')
274            && (first_line.ends_with(':') || first_line.contains(": "))
275            && let Some(attr) = Attribute::parse(source, parser)
276        {
277            let mut warnings: Vec<Warning<'src>> = vec![];
278            parser.set_attribute_from_body(&attr.item, &mut warnings);
279
280            return MatchAndWarnings {
281                item: BlockParseOutcome::Parsed(MatchedItem {
282                    item: Self::DocumentAttribute(attr.item),
283                    after: attr.after,
284                }),
285                warnings,
286            };
287        }
288
289        // Optimization not possible; start by looking for block metadata (title,
290        // attrlist, etc.).
291        let MatchAndWarnings {
292            item: mut metadata,
293            mut warnings,
294        } = BlockMetadata::parse(source, parser);
295
296        // A block title stashed by an enclosing section heading (see
297        // `SectionBlock::parse`) is claimed by the next block parsed – this
298        // one. A title of the block's own wins, discarding the carried title.
299        // The carried title has no source line adjacent to this block, so
300        // `title_source` stays `None` (the same shape as a `title=` attribute).
301        if let Some(pending_title) = parser.pending_block_title.take()
302            && metadata.title.is_none()
303        {
304            // The carried title arrives as an owned snapshot; rebuild it as a
305            // `Content` anchored at the block's start, restoring any deferred
306            // cross-references so the title pass can still resolve them.
307            metadata.title = Some(crate::content::Content::from_owned_title(
308                metadata.block_start,
309                pending_title,
310            ));
311        }
312
313        // Tolerate a blank line between a block's metadata (title, anchor, or
314        // attribute list) and the block it decorates. Asciidoctor's
315        // `parse_block_metadata_lines` skips blank lines after each metadata
316        // line, so metadata separated from its block by one or more blank lines
317        // still attaches to that block rather than dangling as a spurious
318        // `MissingBlockAfterTitleOrAttributeList`. Advancing `block_start` past
319        // the gap lets the block-type dispatch below see the content directly.
320        //
321        // This applies at the block level only. Inside a list item,
322        // blank-separated metadata follows the list-continuation rules handled
323        // in `ListItem::parse` (where such metadata is discarded), so leave
324        // `block_start` pointing at the blank line for those callers. Likewise,
325        // if only blank lines follow (no block content), leave it untouched so
326        // the genuinely-dangling-metadata warning still fires.
327        if parent_list_markers.is_none() && !metadata.is_empty() {
328            let after_blanks = metadata.block_start.discard_empty_lines();
329            if after_blanks != metadata.block_start && !after_blanks.is_empty() {
330                metadata.block_start = after_blanks;
331            }
332        }
333
334        // Resolve attribute references in a `[[id,reftext]]` anchor reftext now,
335        // while the parser still holds the attributes in effect where the anchor
336        // appears. A compound block's body (parsed below) can redefine those
337        // attributes, so deferring this to registration – after the body – would
338        // record the wrong value. The result is threaded into `block_reftext`.
339        let anchor_reftext = metadata
340            .anchor_reftext
341            .as_ref()
342            .map(|span| substitute_attributes_in_reftext(*span, parser));
343
344        // The `[literal]` block style normally marks a literal *paragraph*,
345        // which is handled directly as a simple (literal) block below, bypassing
346        // the delimited-block parsers. The exception is when `[literal]` is set
347        // on the delimiter line of a structural container, where it masquerades
348        // over that container (e.g. `[literal]` on a `----` listing, on a `....`
349        // literal, or on a `--` open block); those cases must fall through to the
350        // delimited-block parsers.
351        let is_literal =
352            metadata.attrlist.as_ref().and_then(|a| a.block_style()) == Some("literal") && {
353                let first_line = metadata.block_start.take_normalized_line().item;
354                !RawDelimitedBlock::is_valid_delimiter(&first_line)
355                    && !CompoundDelimitedBlock::is_valid_delimiter(&first_line)
356                    && !TableBlock::is_table_delimiter(&first_line)
357            };
358
359        // A simple block may be parsed speculatively inside the `!is_literal`
360        // branch below (to detect the "metadata with no block" edge case). When
361        // that speculative parse succeeds it is reused as the final result rather
362        // than re-parsed, so that the captioning side effect of
363        // `SimpleBlock::parse` (which can consume a caption counter) happens at
364        // most once per block.
365        let mut simple_block_mi = None;
366
367        if !is_literal {
368            if let Some(mut adm_maw) = AdmonitionBlock::parse(&metadata, parser)
369                && let Some(adm) = adm_maw.item
370            {
371                if !adm_maw.warnings.is_empty() {
372                    warnings.append(&mut adm_maw.warnings);
373                }
374
375                let block = Self::Admonition(adm.item);
376
377                Self::register_block_id(
378                    block.id(),
379                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
380                    Self::block_signifier(&block, parser),
381                    block.span(),
382                    parser,
383                    &mut warnings,
384                );
385
386                return MatchAndWarnings {
387                    item: BlockParseOutcome::Parsed(MatchedItem {
388                        item: block,
389                        after: adm.after,
390                    }),
391                    warnings,
392                };
393            }
394
395            if let Some(mut quote_maw) = QuoteBlock::parse(&metadata, parser)
396                && let Some(quote) = quote_maw.item
397            {
398                if !quote_maw.warnings.is_empty() {
399                    warnings.append(&mut quote_maw.warnings);
400                }
401
402                let block = Self::Quote(quote.item);
403
404                Self::register_block_id(
405                    block.id(),
406                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
407                    Self::block_signifier(&block, parser),
408                    block.span(),
409                    parser,
410                    &mut warnings,
411                );
412
413                return MatchAndWarnings {
414                    item: BlockParseOutcome::Parsed(MatchedItem {
415                        item: block,
416                        after: quote.after,
417                    }),
418                    warnings,
419                };
420            }
421
422            if let Some(mut rdb_maw) = RawDelimitedBlock::parse(&metadata, parser)
423                && let Some(rdb) = rdb_maw.item
424            {
425                if !rdb_maw.warnings.is_empty() {
426                    warnings.append(&mut rdb_maw.warnings);
427                }
428
429                let block = Self::RawDelimited(rdb.item);
430
431                Self::register_block_id(
432                    block.id(),
433                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
434                    Self::block_signifier(&block, parser),
435                    block.span(),
436                    parser,
437                    &mut warnings,
438                );
439
440                return MatchAndWarnings {
441                    item: BlockParseOutcome::Parsed(MatchedItem {
442                        item: block,
443                        after: rdb.after,
444                    }),
445                    warnings,
446                };
447            }
448
449            if let Some(mut cdb_maw) = CompoundDelimitedBlock::parse(&metadata, parser)
450                && let Some(cdb) = cdb_maw.item
451            {
452                if !cdb_maw.warnings.is_empty() {
453                    warnings.append(&mut cdb_maw.warnings);
454                }
455
456                let block = Self::CompoundDelimited(cdb.item);
457
458                Self::register_block_id(
459                    block.id(),
460                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
461                    Self::block_signifier(&block, parser),
462                    block.span(),
463                    parser,
464                    &mut warnings,
465                );
466
467                return MatchAndWarnings {
468                    item: BlockParseOutcome::Parsed(MatchedItem {
469                        item: block,
470                        after: cdb.after,
471                    }),
472                    warnings,
473                };
474            }
475
476            if let Some(mut table_maw) = TableBlock::parse(&metadata, parser)
477                && let Some(table) = table_maw.item
478            {
479                if !table_maw.warnings.is_empty() {
480                    warnings.append(&mut table_maw.warnings);
481                }
482
483                let block = Self::Table(table.item);
484
485                Self::register_block_id(
486                    block.id(),
487                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
488                    Self::block_signifier(&block, parser),
489                    block.span(),
490                    parser,
491                    &mut warnings,
492                );
493
494                return MatchAndWarnings {
495                    item: BlockParseOutcome::Parsed(MatchedItem {
496                        item: block,
497                        after: table.after,
498                    }),
499                    warnings,
500                };
501            }
502
503            // Try to discern the block type by scanning the first line.
504            let line = metadata.block_start.take_normalized_line();
505
506            if line.item.starts_with("image::")
507                || line.item.starts_with("video::")
508                || line.item.starts_with("audio::")
509            {
510                let mut media_block_maw = MediaBlock::parse(&metadata, parser);
511
512                if let Some(mut media_block) = media_block_maw.item {
513                    // Only propagate warnings from media block parsing if we think this
514                    // *is* a media block. Otherwise, there would likely be too many false
515                    // positives.
516                    if !media_block_maw.warnings.is_empty() {
517                        warnings.append(&mut media_block_maw.warnings);
518                    }
519
520                    // Resolve attribute references in the macro target. Under
521                    // `attribute-missing=drop-line`, a reference to a missing
522                    // attribute drops the entire block (Asciidoctor behavior).
523                    if media_block.item.resolve_target(parser) == TargetResolution::Drop {
524                        return MatchAndWarnings {
525                            item: BlockParseOutcome::Dropped(media_block.after),
526                            warnings,
527                        };
528                    }
529
530                    // Assign the caption only now that the block has survived
531                    // `resolve_target`, so a dropped image does not consume the
532                    // `figure-number` counter and leave a gap in the numbering.
533                    media_block.item.assign_caption(parser);
534
535                    let block = Self::Media(media_block.item);
536
537                    Self::register_block_id(
538                        block.id(),
539                        Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
540                        Self::block_signifier(&block, parser),
541                        block.span(),
542                        parser,
543                        &mut warnings,
544                    );
545
546                    return MatchAndWarnings {
547                        item: BlockParseOutcome::Parsed(MatchedItem {
548                            item: block,
549                            after: media_block.after,
550                        }),
551                        warnings,
552                    };
553                }
554
555                // This might be some other kind of block, so we don't
556                // automatically error out on a parse failure.
557            }
558
559            if line.item.starts_with("toc::") {
560                let mut toc_block_maw = TocBlock::parse(&metadata, parser);
561
562                if let Some(toc_block) = toc_block_maw.item {
563                    // Only propagate warnings from TOC block parsing if we think
564                    // this *is* a TOC block. Otherwise, there would likely be too
565                    // many false positives.
566                    if !toc_block_maw.warnings.is_empty() {
567                        warnings.append(&mut toc_block_maw.warnings);
568                    }
569
570                    let block = Self::Toc(toc_block.item);
571
572                    Self::register_block_id(
573                        block.id(),
574                        Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
575                        Self::block_signifier(&block, parser),
576                        block.span(),
577                        parser,
578                        &mut warnings,
579                    );
580
581                    return MatchAndWarnings {
582                        item: BlockParseOutcome::Parsed(MatchedItem {
583                            item: block,
584                            after: toc_block.after,
585                        }),
586                        warnings,
587                    };
588                }
589
590                // This might be some other kind of block, so we don't
591                // automatically error out on a parse failure.
592            }
593
594            if (line.item.starts_with('=') || line.item.starts_with('#'))
595                && let Some(mi_section_block) =
596                    SectionBlock::parse(&metadata, parser, &mut warnings)
597            {
598                // A line starting with `=` or `#` might be some other kind of block, so we
599                // continue quietly if `SectionBlock` parser rejects this block.
600
601                return MatchAndWarnings {
602                    item: BlockParseOutcome::Parsed(MatchedItem {
603                        item: Self::Section(mi_section_block.item),
604                        after: mi_section_block.after,
605                    }),
606                    warnings,
607                };
608            }
609
610            if (line.item.starts_with('\'')
611                || line.item.starts_with('-')
612                || line.item.starts_with('*')
613                || line.item.starts_with('_')
614                || line.item.starts_with('<'))
615                && let Some(mi_break) = Break::parse(&metadata, parser)
616            {
617                // Continue quietly if `Break` parser rejects this block.
618
619                return MatchAndWarnings {
620                    item: BlockParseOutcome::Parsed(MatchedItem {
621                        item: Self::Break(mi_break.item),
622                        after: mi_break.after,
623                    }),
624                    warnings,
625                };
626            }
627
628            // Only try to parse as a new list if we're NOT inside a list item context.
629            // If we are inside a list context, lists can only be created when the first
630            // line is a list item marker (handled above).
631            if parent_list_markers.is_none()
632                && let Some(mi_list) = ListBlock::parse(&metadata, parser, &mut warnings)
633            {
634                return MatchAndWarnings {
635                    item: BlockParseOutcome::Parsed(MatchedItem {
636                        item: Self::List(mi_list.item),
637                        after: mi_list.after,
638                    }),
639                    warnings,
640                };
641            }
642
643            // First, let's look for a fun edge case. Perhaps the text contains block
644            // metadata but no block immediately following. If we're not careful, we could
645            // spin in a loop (for example, `parse_blocks_until`) thinking there will be
646            // another block, but there isn't.
647
648            // The following check disables that spin loop.
649            simple_block_mi = if let Some(plm) = parent_list_markers {
650                SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
651            } else {
652                SimpleBlock::parse(&metadata, parser)
653            };
654
655            if simple_block_mi.is_none() && !metadata.is_empty() {
656                // We have a metadata with no block. Treat it as a simple block but issue a
657                // warning.
658
659                warnings.push(Warning {
660                    source: metadata.source,
661                    warning: WarningType::MissingBlockAfterTitleOrAttributeList,
662                    origin: None,
663                });
664
665                // Remove the metadata content so that SimpleBlock will read the title/attrlist
666                // line(s) as regular content. The speculative parse failed, so the
667                // block is re-parsed below with this stripped metadata.
668                metadata.title_source = None;
669                metadata.title = None;
670                metadata.anchor = None;
671                metadata.attrlist = None;
672                metadata.block_start = metadata.source;
673            }
674        }
675
676        // If no other block kind matches, we can always use SimpleBlock. Reuse the
677        // speculative parse from the `!is_literal` branch when it succeeded;
678        // otherwise (a literal block, or metadata stripped above) parse now.
679        let simple_block_mi = match simple_block_mi {
680            Some(mi) => Some(mi),
681            None => {
682                if let Some(plm) = parent_list_markers {
683                    SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
684                } else {
685                    SimpleBlock::parse(&metadata, parser)
686                }
687            }
688        };
689
690        let mut result = MatchAndWarnings {
691            item: match simple_block_mi {
692                Some(mi) => BlockParseOutcome::Parsed(MatchedItem {
693                    item: Self::Simple(mi.item),
694                    after: mi.after,
695                }),
696                None => BlockParseOutcome::NoMatch,
697            },
698            warnings,
699        };
700
701        if let BlockParseOutcome::Parsed(ref matched_item) = result.item {
702            Self::register_block_id(
703                matched_item.item.id(),
704                Self::block_reftext(&matched_item.item, anchor_reftext.as_deref()).as_deref(),
705                Self::block_signifier(&matched_item.item, parser),
706                matched_item.item.span(),
707                parser,
708                &mut result.warnings,
709            );
710        }
711
712        result
713    }
714
715    /// Determine the [`XrefSignifier`] a cross-reference uses to build
716    /// `full`/`short` [`xrefstyle`](crate::parser::XrefStyle) text when this
717    /// block is the target.
718    ///
719    /// A signifier is produced only for an auto-numbered captioned block (e.g.
720    /// an image → "Figure 1", a titled table → "Table 1") that has no explicit
721    /// reftext. A block with an explicit `reftext` attribute or a
722    /// `[[id,reftext]]` anchor reftext uses that text verbatim, so it gets no
723    /// signifier; neither does an uncaptioned block or one whose caption was
724    /// overridden with `[caption=...]` (which is not numbered).
725    fn block_signifier<'a>(block: &'a Block<'a>, parser: &Parser) -> Option<XrefSignifier> {
726        // Only captioned blocks are eligible.
727        let caption = block.caption()?;
728
729        let has_explicit_reftext = block
730            .attrlist()
731            .and_then(|attrlist| attrlist.named_attribute("reftext"))
732            .is_some()
733            || block.anchor_reftext().is_some();
734        if has_explicit_reftext {
735            return None;
736        }
737
738        // Exclude explicit caption overrides, which are not numbered. This is
739        // *not* the same as `block.number().is_none()`: an auto-numbered block
740        // whose context counter holds a non-integer value (e.g. `:figure-number:
741        // A`, rendering "Figure B") also has no bare integer number, yet it is
742        // genuinely numbered and must keep its signifier ("Figure B").
743        if Self::has_caption_override(block, parser) {
744            return None;
745        }
746
747        // The caption prefix is "<label> <n>. "; the xrefstyle label is that
748        // prefix without its trailing ". " separator (e.g. "Figure 1").
749        let label = caption.strip_suffix(". ").unwrap_or(caption).to_string();
750        Some(XrefSignifier {
751            label,
752            emphasize: false,
753        })
754    }
755
756    /// Whether a captioned block's caption comes from an explicit override
757    /// rather than automatic numbering.
758    ///
759    /// An override is a `caption` attribute on the block (or, for an image, on
760    /// the image macro), or a non-empty document-wide `caption` attribute. This
761    /// mirrors the override detection in
762    /// [`caption::assign_block_caption`](crate::blocks::caption) and
763    /// [`MediaBlock::assign_caption`], so the two agree on which blocks are
764    /// numbered.
765    fn has_caption_override<'a>(block: &'a Block<'a>, parser: &Parser) -> bool {
766        let attribute_override = block
767            .attrlist()
768            .and_then(|attrlist| attrlist.named_attribute("caption"))
769            .is_some()
770            || matches!(block, Block::Media(media)
771                if media.macro_attrlist().named_attribute("caption").is_some());
772
773        attribute_override
774            || matches!(
775                parser.attribute_value("caption"),
776                InterpretedValue::Value(value) if !value.is_empty(),
777            )
778    }
779
780    /// Determine the reftext (a.k.a. xreflabel) used as the link text when a
781    /// block is the target of a cross reference. Asciidoctor's precedence is:
782    /// an explicit `reftext` attribute, then the reftext supplied with a
783    /// block anchor (`[[id,reftext]]`), and finally the block title.
784    ///
785    /// `anchor_reftext` is the block's `[[id,reftext]]` anchor reftext with its
786    /// attribute references already resolved (by the caller, against the
787    /// attributes in effect where the anchor appears – captured before the
788    /// block's body is parsed, since a compound block's body may itself
789    /// redefine those attributes). This matches how the anchor ID and a
790    /// `reftext=` attribute (both substituted when the attribute list is
791    /// parsed) are handled; the `reftext=` and title branches are already
792    /// substituted.
793    fn block_reftext<'a>(block: &'a Block<'a>, anchor_reftext: Option<&str>) -> Option<CowStr<'a>> {
794        if let Some(attr) = block
795            .attrlist()
796            .and_then(|attrlist| attrlist.named_attribute("reftext"))
797        {
798            return Some(CowStr::from(attr.value()));
799        }
800
801        if let Some(anchor_reftext) = anchor_reftext {
802            return Some(CowStr::from(anchor_reftext.to_string()));
803        }
804
805        block.title().map(CowStr::from)
806    }
807
808    /// Register a block's ID with the catalog if the block has an ID.
809    ///
810    /// This should be called for all block types except `SectionBlock`,
811    /// which handles its own catalog registration.
812    fn register_block_id(
813        id: Option<&str>,
814        reftext: Option<&str>,
815        signifier: Option<XrefSignifier>,
816        span: Span<'src>,
817        parser: &mut Parser,
818        warnings: &mut Vec<Warning<'src>>,
819    ) {
820        if let Some(id) = id {
821            match parser.register_ref(id, reftext, RefType::Anchor) {
822                Ok(()) => {
823                    if let Some(signifier) = signifier {
824                        parser.set_ref_signifier(id, signifier);
825                    }
826                }
827                Err(_duplicate_error) => {
828                    // If registration fails due to duplicate ID, issue a warning.
829                    warnings.push(Warning {
830                        source: span,
831                        warning: WarningType::DuplicateId(id.to_string()),
832                        origin: None,
833                    });
834                }
835            }
836        }
837    }
838
839    /// Returns a reference to the inner [`ListItem`] if this is a
840    /// `Block::ListItem`, or `None` otherwise.
841    pub(crate) fn as_list_item(&self) -> Option<&ListItem<'src>> {
842        match self {
843            Self::ListItem(li) => Some(li),
844            _ => None,
845        }
846    }
847
848    /// Resolve any deferred cross-references in this block and its descendants,
849    /// using `resolver` to map targets to destinations and `renderer` to render
850    /// the resulting links. Unresolved targets are reported in `warnings`.
851    ///
852    /// This drives the recursion uniformly via the [`IsBlock::content_mut`] and
853    /// [`IsBlock::child_blocks_mut`] accessors, so it needs no per-block-type
854    /// special casing.
855    pub(crate) fn resolve_references(
856        &mut self,
857        resolver: &dyn ReferenceResolver,
858        renderer: &dyn InlineSubstitutionRenderer,
859        warnings: &mut ReferenceWarnings<'src>,
860    ) {
861        // A section is not resolved here: its resolvable content is its
862        // heading, which `content_mut` deliberately does not expose (see
863        // `SectionBlock`). Headings are resolved by the document-order title
864        // pass (`title_refs::resolve_title_references`), which coordinates
865        // cross-references *between* titles (forward and circular) – something
866        // per-content resolution cannot see.
867        if let Some(content) = self.content_mut() {
868            content.resolve_references(resolver, renderer, warnings);
869        }
870
871        // Tables hold their resolvable content in cells rather than in a single
872        // `content_mut()` value, so they are resolved explicitly here.
873        if let Self::Table(table) = self {
874            table.resolve_references(resolver, renderer, warnings);
875        }
876
877        // A Markdown-style blockquote holds its nested blocks in its own owned
878        // source, which the generic `child_blocks_mut()` walk below does not
879        // reach, so they are resolved explicitly here.
880        if let Self::Quote(quote) = self {
881            quote.resolve_references(resolver, renderer, warnings);
882        }
883
884        for child in self.child_blocks_mut() {
885            child.resolve_references(resolver, renderer, warnings);
886        }
887    }
888
889    /// Returns this block's *block title* (`.Title`) as a mutable [`Content`],
890    /// when the block has one.
891    ///
892    /// This is the decorative title carried above a block, distinct from a
893    /// section's heading. Used only by the document-order title resolution
894    /// pass, which reads a title's deferred cross-references and installs the
895    /// re-rendered title once they are resolved. Blocks that never carry a
896    /// title return `None`.
897    pub(crate) fn block_title_content_mut(&mut self) -> Option<&mut Content<'src>> {
898        match self {
899            Self::Simple(b) => b.title_content_mut(),
900            Self::Media(b) => b.title_content_mut(),
901            Self::List(b) => b.title_content_mut(),
902            Self::RawDelimited(b) => b.title_content_mut(),
903            Self::CompoundDelimited(b) => b.title_content_mut(),
904            Self::Admonition(b) => b.title_content_mut(),
905            Self::Quote(b) => b.title_content_mut(),
906            Self::Table(b) => b.title_content_mut(),
907            Self::Break(b) => b.title_content_mut(),
908            Self::Toc(b) => b.title_content_mut(),
909            _ => None,
910        }
911    }
912}
913
914impl<'src> IsBlock<'src> for Block<'src> {
915    fn content_model(&self) -> ContentModel {
916        match self {
917            Self::Simple(_) => ContentModel::Simple,
918            Self::Media(b) => b.content_model(),
919            Self::Section(_) => ContentModel::Compound,
920            Self::List(b) => b.content_model(),
921            Self::ListItem(b) => b.content_model(),
922            Self::RawDelimited(b) => b.content_model(),
923            Self::CompoundDelimited(b) => b.content_model(),
924            Self::Admonition(b) => b.content_model(),
925            Self::Quote(b) => b.content_model(),
926            Self::Table(b) => b.content_model(),
927            Self::Preamble(b) => b.content_model(),
928            Self::Break(b) => b.content_model(),
929            Self::Toc(b) => b.content_model(),
930            Self::DocumentAttribute(b) => b.content_model(),
931        }
932    }
933
934    fn declared_style(&'src self) -> Option<&'src str> {
935        match self {
936            Self::Simple(b) => b.declared_style(),
937            Self::Media(b) => b.declared_style(),
938            Self::Section(b) => b.declared_style(),
939            Self::List(b) => b.declared_style(),
940            Self::ListItem(b) => b.declared_style(),
941            Self::RawDelimited(b) => b.declared_style(),
942            Self::CompoundDelimited(b) => b.declared_style(),
943            Self::Admonition(b) => b.declared_style(),
944            Self::Quote(b) => b.declared_style(),
945            Self::Table(b) => b.declared_style(),
946            Self::Preamble(b) => b.declared_style(),
947            Self::Break(b) => b.declared_style(),
948            Self::Toc(b) => b.declared_style(),
949            Self::DocumentAttribute(b) => b.declared_style(),
950        }
951    }
952
953    fn rendered_content(&'src self) -> Option<&'src str> {
954        match self {
955            Self::Simple(b) => b.rendered_content(),
956            Self::Media(b) => b.rendered_content(),
957            Self::Section(b) => b.rendered_content(),
958            Self::List(b) => b.rendered_content(),
959            Self::ListItem(b) => b.rendered_content(),
960            Self::RawDelimited(b) => b.rendered_content(),
961            Self::CompoundDelimited(b) => b.rendered_content(),
962            Self::Admonition(b) => b.rendered_content(),
963            Self::Quote(b) => b.rendered_content(),
964            Self::Table(b) => b.rendered_content(),
965            Self::Preamble(b) => b.rendered_content(),
966            Self::Break(b) => b.rendered_content(),
967            Self::Toc(b) => b.rendered_content(),
968            Self::DocumentAttribute(b) => b.rendered_content(),
969        }
970    }
971
972    fn raw_context(&self) -> CowStr<'src> {
973        match self {
974            Self::Simple(b) => b.raw_context(),
975            Self::Media(b) => b.raw_context(),
976            Self::Section(b) => b.raw_context(),
977            Self::List(b) => b.raw_context(),
978            Self::ListItem(b) => b.raw_context(),
979            Self::RawDelimited(b) => b.raw_context(),
980            Self::CompoundDelimited(b) => b.raw_context(),
981            Self::Admonition(b) => b.raw_context(),
982            Self::Quote(b) => b.raw_context(),
983            Self::Table(b) => b.raw_context(),
984            Self::Preamble(b) => b.raw_context(),
985            Self::Break(b) => b.raw_context(),
986            Self::Toc(b) => b.raw_context(),
987            Self::DocumentAttribute(b) => b.raw_context(),
988        }
989    }
990
991    fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
992        match self {
993            Self::Simple(b) => b.child_blocks_mut(),
994            Self::Media(b) => b.child_blocks_mut(),
995            Self::Section(b) => b.child_blocks_mut(),
996            Self::List(b) => b.child_blocks_mut(),
997            Self::ListItem(b) => b.child_blocks_mut(),
998            Self::RawDelimited(b) => b.child_blocks_mut(),
999            Self::CompoundDelimited(b) => b.child_blocks_mut(),
1000            Self::Admonition(b) => b.child_blocks_mut(),
1001            Self::Quote(b) => b.child_blocks_mut(),
1002            Self::Table(b) => b.child_blocks_mut(),
1003            Self::Preamble(b) => b.child_blocks_mut(),
1004            Self::Break(b) => b.child_blocks_mut(),
1005            Self::Toc(b) => b.child_blocks_mut(),
1006            Self::DocumentAttribute(b) => b.child_blocks_mut(),
1007        }
1008    }
1009
1010    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
1011        match self {
1012            Self::Simple(b) => b.content_mut(),
1013            Self::Media(b) => b.content_mut(),
1014            Self::Section(b) => b.content_mut(),
1015            Self::List(b) => b.content_mut(),
1016            Self::ListItem(b) => b.content_mut(),
1017            Self::RawDelimited(b) => b.content_mut(),
1018            Self::CompoundDelimited(b) => b.content_mut(),
1019            Self::Admonition(b) => b.content_mut(),
1020            Self::Quote(b) => b.content_mut(),
1021            Self::Table(b) => b.content_mut(),
1022            Self::Preamble(b) => b.content_mut(),
1023            Self::Break(b) => b.content_mut(),
1024            Self::Toc(b) => b.content_mut(),
1025            Self::DocumentAttribute(b) => b.content_mut(),
1026        }
1027    }
1028
1029    fn title_source(&'src self) -> Option<Span<'src>> {
1030        match self {
1031            Self::Simple(b) => b.title_source(),
1032            Self::Media(b) => b.title_source(),
1033            Self::Section(b) => b.title_source(),
1034            Self::List(b) => b.title_source(),
1035            Self::ListItem(b) => b.title_source(),
1036            Self::RawDelimited(b) => b.title_source(),
1037            Self::CompoundDelimited(b) => b.title_source(),
1038            Self::Admonition(b) => b.title_source(),
1039            Self::Quote(b) => b.title_source(),
1040            Self::Table(b) => b.title_source(),
1041            Self::Preamble(b) => b.title_source(),
1042            Self::Break(b) => b.title_source(),
1043            Self::Toc(b) => b.title_source(),
1044            Self::DocumentAttribute(b) => b.title_source(),
1045        }
1046    }
1047
1048    fn title(&self) -> Option<&str> {
1049        match self {
1050            Self::Simple(b) => b.title(),
1051            Self::Media(b) => b.title(),
1052            Self::Section(b) => b.title(),
1053            Self::List(b) => b.title(),
1054            Self::ListItem(b) => b.title(),
1055            Self::RawDelimited(b) => b.title(),
1056            Self::CompoundDelimited(b) => b.title(),
1057            Self::Admonition(b) => b.title(),
1058            Self::Quote(b) => b.title(),
1059            Self::Table(b) => b.title(),
1060            Self::Preamble(b) => b.title(),
1061            Self::Break(b) => b.title(),
1062            Self::Toc(b) => b.title(),
1063            Self::DocumentAttribute(b) => b.title(),
1064        }
1065    }
1066
1067    fn caption(&self) -> Option<&str> {
1068        match self {
1069            Self::Simple(b) => b.caption(),
1070            Self::Media(b) => b.caption(),
1071            Self::Section(b) => b.caption(),
1072            Self::List(b) => b.caption(),
1073            Self::ListItem(b) => b.caption(),
1074            Self::RawDelimited(b) => b.caption(),
1075            Self::CompoundDelimited(b) => b.caption(),
1076            Self::Admonition(b) => b.caption(),
1077            Self::Quote(b) => b.caption(),
1078            Self::Table(b) => b.caption(),
1079            Self::Preamble(b) => b.caption(),
1080            Self::Break(b) => b.caption(),
1081            Self::Toc(b) => b.caption(),
1082            Self::DocumentAttribute(b) => b.caption(),
1083        }
1084    }
1085
1086    fn number(&self) -> Option<usize> {
1087        match self {
1088            Self::Simple(b) => b.number(),
1089            Self::Media(b) => b.number(),
1090            Self::Section(b) => b.number(),
1091            Self::List(b) => b.number(),
1092            Self::ListItem(b) => b.number(),
1093            Self::RawDelimited(b) => b.number(),
1094            Self::CompoundDelimited(b) => b.number(),
1095            Self::Admonition(b) => b.number(),
1096            Self::Quote(b) => b.number(),
1097            Self::Table(b) => b.number(),
1098            Self::Preamble(b) => b.number(),
1099            Self::Break(b) => b.number(),
1100            Self::Toc(b) => b.number(),
1101            Self::DocumentAttribute(b) => b.number(),
1102        }
1103    }
1104
1105    fn id(&'src self) -> Option<&'src str> {
1106        // Three variants override the trait default:
1107        //
1108        // * A `MediaBlock` additionally recognizes a named `id=` _inside_ its macro
1109        //   attribute list (e.g. `image::sunset.jpg[id=sunset-img]`).
1110        //
1111        // * A `TocBlock` likewise recognizes a named `id=` _inside_ its macro attribute
1112        //   list (e.g. `toc::[id=contents]`).
1113        //
1114        // * A `SectionBlock` falls back to its auto-generated (`_slug`) ID when no
1115        //   explicit ID was supplied, so `block.id()` yields the same ID the section is
1116        //   registered and cross-referenced under. Delegating here (rather than
1117        //   applying the trait default) avoids the footgun of `block.id()` silently
1118        //   returning `None` for a section that plainly has an ID.
1119        //
1120        // Every other variant keeps the trait default (explicit anchor or block
1121        // attribute list only).
1122        match self {
1123            Self::Media(b) => b.id(),
1124            Self::Section(b) => b.id(),
1125            Self::Toc(b) => b.id(),
1126            _ => self
1127                .anchor()
1128                .map(|a| a.data())
1129                .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id())),
1130        }
1131    }
1132
1133    fn anchor(&'src self) -> Option<Span<'src>> {
1134        match self {
1135            Self::Simple(b) => b.anchor(),
1136            Self::Media(b) => b.anchor(),
1137            Self::Section(b) => b.anchor(),
1138            Self::List(b) => b.anchor(),
1139            Self::ListItem(b) => b.anchor(),
1140            Self::RawDelimited(b) => b.anchor(),
1141            Self::CompoundDelimited(b) => b.anchor(),
1142            Self::Admonition(b) => b.anchor(),
1143            Self::Quote(b) => b.anchor(),
1144            Self::Table(b) => b.anchor(),
1145            Self::Preamble(b) => b.anchor(),
1146            Self::Break(b) => b.anchor(),
1147            Self::Toc(b) => b.anchor(),
1148            Self::DocumentAttribute(b) => b.anchor(),
1149        }
1150    }
1151
1152    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
1153        match self {
1154            Self::Simple(b) => b.anchor_reftext(),
1155            Self::Media(b) => b.anchor_reftext(),
1156            Self::Section(b) => b.anchor_reftext(),
1157            Self::List(b) => b.anchor_reftext(),
1158            Self::ListItem(b) => b.anchor_reftext(),
1159            Self::RawDelimited(b) => b.anchor_reftext(),
1160            Self::CompoundDelimited(b) => b.anchor_reftext(),
1161            Self::Admonition(b) => b.anchor_reftext(),
1162            Self::Quote(b) => b.anchor_reftext(),
1163            Self::Table(b) => b.anchor_reftext(),
1164            Self::Preamble(b) => b.anchor_reftext(),
1165            Self::Break(b) => b.anchor_reftext(),
1166            Self::Toc(b) => b.anchor_reftext(),
1167            Self::DocumentAttribute(b) => b.anchor_reftext(),
1168        }
1169    }
1170
1171    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
1172        match self {
1173            Self::Simple(b) => b.attrlist(),
1174            Self::Media(b) => b.attrlist(),
1175            Self::Section(b) => b.attrlist(),
1176            Self::List(b) => b.attrlist(),
1177            Self::ListItem(b) => b.attrlist(),
1178            Self::RawDelimited(b) => b.attrlist(),
1179            Self::CompoundDelimited(b) => b.attrlist(),
1180            Self::Admonition(b) => b.attrlist(),
1181            Self::Quote(b) => b.attrlist(),
1182            Self::Table(b) => b.attrlist(),
1183            Self::Preamble(b) => b.attrlist(),
1184            Self::Break(b) => b.attrlist(),
1185            Self::Toc(b) => b.attrlist(),
1186            Self::DocumentAttribute(b) => b.attrlist(),
1187        }
1188    }
1189
1190    fn substitution_group(&self) -> SubstitutionGroup {
1191        match self {
1192            Self::Simple(b) => b.substitution_group(),
1193            Self::Media(b) => b.substitution_group(),
1194            Self::Section(b) => b.substitution_group(),
1195            Self::List(b) => b.substitution_group(),
1196            Self::ListItem(b) => b.substitution_group(),
1197            Self::RawDelimited(b) => b.substitution_group(),
1198            Self::CompoundDelimited(b) => b.substitution_group(),
1199            Self::Admonition(b) => b.substitution_group(),
1200            Self::Quote(b) => b.substitution_group(),
1201            Self::Table(b) => b.substitution_group(),
1202            Self::Preamble(b) => b.substitution_group(),
1203            Self::Break(b) => b.substitution_group(),
1204            Self::Toc(b) => b.substitution_group(),
1205            Self::DocumentAttribute(b) => b.substitution_group(),
1206        }
1207    }
1208}
1209
1210impl<'src> HasSpan<'src> for Block<'src> {
1211    fn span(&self) -> Span<'src> {
1212        match self {
1213            Self::Simple(b) => b.span(),
1214            Self::Media(b) => b.span(),
1215            Self::Section(b) => b.span(),
1216            Self::List(b) => b.span(),
1217            Self::ListItem(b) => b.span(),
1218            Self::RawDelimited(b) => b.span(),
1219            Self::CompoundDelimited(b) => b.span(),
1220            Self::Admonition(b) => b.span(),
1221            Self::Quote(b) => b.span(),
1222            Self::Table(b) => b.span(),
1223            Self::Preamble(b) => b.span(),
1224            Self::Break(b) => b.span(),
1225            Self::Toc(b) => b.span(),
1226            Self::DocumentAttribute(b) => b.span(),
1227        }
1228    }
1229}