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