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