Skip to main content

asciidoc_parser/blocks/
block.rs

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