Skip to main content

asciidoc_parser/blocks/
block.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{
5        AdmonitionBlock, Break, CompoundDelimitedBlock, ContentModel, IsBlock, ListBlock, ListItem,
6        ListItemMarker, MediaBlock, Preamble, QuoteBlock, RawDelimitedBlock, SectionBlock,
7        SimpleBlock, TableBlock, TocBlock, is_built_in_context, media::TargetResolution,
8        metadata::BlockMetadata, starts_with_admonition_label,
9    },
10    content::{Content, SubstitutionGroup, substitute_attributes_in_reftext},
11    document::{Attribute, InterpretedValue, RefType},
12    parser::{InlineSubstitutionRenderer, ReferenceResolver, ReferenceWarnings, XrefSignifier},
13    span::MatchedItem,
14    strings::CowStr,
15    warnings::{MatchAndWarnings, Warning, WarningType},
16};
17
18/// **Block elements** form the main structure of an AsciiDoc document, starting
19/// with the document itself.
20///
21/// A block element (aka **block**) is a discrete, line-oriented chunk of
22/// content in an AsciiDoc document. Once parsed, that chunk of content becomes
23/// a block element in the parsed document model. Certain blocks may contain
24/// other blocks, so we say that blocks can be nested. The converter visits each
25/// block in turn, in document order, converting it to a corresponding chunk of
26/// output.
27///
28/// This enum represents all of the block types that are understood directly by
29/// this parser and also implements the [`IsBlock`] trait.
30#[derive(Clone, Eq, Hash, PartialEq)]
31#[non_exhaustive]
32pub enum Block<'src> {
33    /// A block that’s treated as contiguous lines of paragraph text (and
34    /// subject to normal substitutions) (e.g., a paragraph block).
35    Simple(SimpleBlock<'src>),
36
37    /// A media block is used to represent an image, video, or audio block
38    /// macro.
39    Media(MediaBlock<'src>),
40
41    /// A section helps to partition the document into a content hierarchy.
42    /// May also be a part, chapter, or special section.
43    Section(SectionBlock<'src>),
44
45    /// A list contains a sequence of items prefixed with symbol, such as a disc
46    /// (aka bullet). Each individual item in the list is represented by a
47    /// [`ListItem`].
48    List(ListBlock<'src>),
49
50    /// A list item is a special kind of block that is a member of a
51    /// [`ListBlock`] and contains one or more blocks attached to it.
52    ListItem(ListItem<'src>),
53
54    /// A delimited block that contains verbatim, raw, or comment text. The
55    /// content between the matching delimiters is not parsed for block
56    /// syntax.
57    RawDelimited(RawDelimitedBlock<'src>),
58
59    /// A delimited block that can contain other blocks.
60    CompoundDelimited(CompoundDelimitedBlock<'src>),
61
62    /// An admonition draws attention to a statement by taking it out of the
63    /// content's flow and labeling it with a priority (e.g., a note or a
64    /// warning).
65    Admonition(AdmonitionBlock<'src>),
66
67    /// A blockquote: a quote, prose excerpt, or verse, optionally attributed to
68    /// a person and a source citation.
69    Quote(QuoteBlock<'src>),
70
71    /// A table block arranges content into a grid of rows and columns.
72    Table(TableBlock<'src>),
73
74    /// Content between the end of the document header and the first section
75    /// title in the document body is called the preamble.
76    Preamble(Preamble<'src>),
77
78    /// A thematic or page break.
79    Break(Break<'src>),
80
81    /// The `toc::[]` block macro, marking where a table of contents should be
82    /// rendered under `toc-placement: macro`.
83    Toc(TocBlock<'src>),
84
85    /// When an attribute is defined in the document body using an attribute
86    /// entry, that’s simply referred to as a document attribute.
87    DocumentAttribute(Attribute<'src>),
88}
89
90impl<'src> std::fmt::Debug for Block<'src> {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Block::Simple(block) => f.debug_tuple("Block::Simple").field(block).finish(),
94            Block::Media(block) => f.debug_tuple("Block::Media").field(block).finish(),
95            Block::Section(block) => f.debug_tuple("Block::Section").field(block).finish(),
96            Block::List(block) => f.debug_tuple("Block::List").field(block).finish(),
97            Block::ListItem(block) => f.debug_tuple("Block::ListItem").field(block).finish(),
98
99            Block::RawDelimited(block) => {
100                f.debug_tuple("Block::RawDelimited").field(block).finish()
101            }
102
103            Block::CompoundDelimited(block) => f
104                .debug_tuple("Block::CompoundDelimited")
105                .field(block)
106                .finish(),
107
108            Block::Admonition(block) => f.debug_tuple("Block::Admonition").field(block).finish(),
109            Block::Quote(block) => f.debug_tuple("Block::Quote").field(block).finish(),
110            Block::Table(block) => f.debug_tuple("Block::Table").field(block).finish(),
111            Block::Preamble(block) => f.debug_tuple("Block::Preamble").field(block).finish(),
112            Block::Break(break_) => f.debug_tuple("Block::Break").field(break_).finish(),
113            Block::Toc(block) => f.debug_tuple("Block::Toc").field(block).finish(),
114
115            Block::DocumentAttribute(block) => f
116                .debug_tuple("Block::DocumentAttribute")
117                .field(block)
118                .finish(),
119        }
120    }
121}
122
123/// Outcome of attempting to parse a single [`Block`].
124///
125/// Most blocks parse to [`Parsed`](Self::Parsed). [`Dropped`](Self::Dropped)
126/// covers input that was consumed but yields no block – a drop-line block
127/// macro, or block metadata (such as a lone empty `[[]]` anchor) that decorates
128/// no block – which the parser must distinguish both from a successful parse
129/// and from "no block matched" (so the block-collection loops advance past the
130/// consumed source rather than spinning or mis-parsing it).
131// `Parsed` embeds a `Block`, which is itself a large enum (see the matching
132// allow on `Block`). This outcome is short-lived and returned by value on the
133// hot parse path, so boxing it would just trade the size for an allocation.
134#[allow(clippy::large_enum_variant)]
135pub(crate) enum BlockParseOutcome<'src> {
136    /// A block was parsed.
137    Parsed(MatchedItem<'src, Block<'src>>),
138
139    /// The input was consumed but yielded no block, so parsing must resume at
140    /// the contained span (where the consumed input ends) rather than treat the
141    /// source as unmatched. Two cases produce this:
142    ///
143    /// * A block macro whose target referenced a missing attribute under
144    ///   `attribute-missing=drop-line`, which Asciidoctor discards entirely.
145    ///
146    /// * Block metadata that named nothing and decorates no block – notably a
147    ///   lone empty `[[]]` anchor at the end of a block scope – which is
148    ///   dropped rather than rendered.
149    Dropped(Span<'src>),
150
151    /// No block matched. This happens only for empty or all-blank input.
152    NoMatch,
153}
154
155impl<'src> Block<'src> {
156    /// Parse a block of any type and return a `Block` that describes it.
157    ///
158    /// Consumes any blank lines before and after the block.
159    ///
160    /// This is a test-only convenience wrapper over
161    /// [`parse_with_outcome`](Self::parse_with_outcome) that flattens the
162    /// drop-line outcome to an `Option`; production code uses
163    /// `parse_with_outcome` so it can react to a dropped block.
164    #[cfg(test)]
165    pub(crate) fn parse(
166        source: Span<'src>,
167        parser: &mut Parser,
168    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
169        let MatchAndWarnings { item, warnings } = Self::parse_internal(source, parser, None, false);
170
171        MatchAndWarnings {
172            item: match item {
173                BlockParseOutcome::Parsed(mi) => Some(mi),
174                BlockParseOutcome::Dropped(_) | BlockParseOutcome::NoMatch => None,
175            },
176            warnings,
177        }
178    }
179
180    /// Parse a block of any type, returning the full [`BlockParseOutcome`] so a
181    /// block-collection loop can advance past a block that was dropped at parse
182    /// time (`attribute-missing=drop-line`). Consumes any blank lines before
183    /// and after the block.
184    ///
185    /// This is the entry point used by production block-collection loops.
186    pub(crate) fn parse_with_outcome(
187        source: Span<'src>,
188        parser: &mut Parser,
189    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
190        Self::parse_internal(source, parser, None, false)
191    }
192
193    /// Parse a block of any type and return a `Block` that describes it.
194    ///
195    /// Will terminate early when parsing certain block types within a list
196    /// context.
197    ///
198    /// Consumes any blank lines before and after the block.
199    ///
200    /// If `is_continuation` is true, this content was attached via a `+`
201    /// continuation marker and literal blocks should preserve their
202    /// indentation.
203    pub(crate) fn parse_for_list_item(
204        source: Span<'src>,
205        parser: &mut Parser,
206        parent_list_markers: &[ListItemMarker<'src>],
207        is_continuation: bool,
208    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
209        Self::parse_internal(source, parser, Some(parent_list_markers), is_continuation)
210    }
211
212    /// Shared parser for [`parse_with_outcome`](Self::parse_with_outcome) and
213    /// [`parse_for_list_item`](Self::parse_for_list_item).
214    ///
215    /// This wraps [`parse_internal_inner`](Self::parse_internal_inner) so that
216    /// every parsed block – whichever branch produced it – passes through the
217    /// unknown-block-style check on the way out. Every block is born here (the
218    /// block-collection loops call one of the public `parse_*` entry points,
219    /// which funnel into this), so this is the single point at which a declared
220    /// style that this parser could not act on is diagnosed.
221    fn parse_internal(
222        source: Span<'src>,
223        parser: &mut Parser,
224        parent_list_markers: Option<&[ListItemMarker<'src>]>,
225        is_continuation: bool,
226    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
227        // The span at which the block's content begins, once its metadata (title,
228        // anchor, attribute list) has been read. `parse_internal_inner` fills this
229        // in so an unknown-style diagnostic can anchor at the block's delimiter or
230        // first content line rather than at the preceding style-attribute line.
231        let mut content_start: Option<Span<'src>> = None;
232
233        let mut result = Self::parse_internal_inner(
234            source,
235            parser,
236            parent_list_markers,
237            is_continuation,
238            &mut content_start,
239        );
240
241        // Record a DEBUG-severity diagnostic when a block declared a style this
242        // parser does not recognize for its context. The block keeps the
243        // default context implied by its syntax (the style is retained but
244        // otherwise ignored); Asciidoctor logs the same condition at debug
245        // level (below its default WARN threshold), so this is surfaced only
246        // for a host that opts in to low-severity diagnostics.
247        if let BlockParseOutcome::Parsed(matched_item) = &result.item
248            && let Some(warning) = unknown_block_style_warning(&matched_item.item)
249        {
250            // Anchor the diagnostic at the block's first content line – the
251            // delimiter of a delimited block, or the opening line of a paragraph
252            // – matching Asciidoctor, which reports `unknown style for …` at that
253            // line rather than at the `[style]` line above it. `content_start` is
254            // always set when a block carries a declared style (that requires an
255            // attribute list, which only the metadata path parses).
256            let span = content_start
257                .unwrap_or_else(|| matched_item.item.span())
258                .take_normalized_line()
259                .item;
260
261            result.warnings.push(Warning::new(span, warning));
262        }
263
264        result
265    }
266
267    /// Shared parser body for [`parse_internal`](Self::parse_internal).
268    fn parse_internal_inner(
269        source: Span<'src>,
270        parser: &mut Parser,
271        parent_list_markers: Option<&[ListItemMarker<'src>]>,
272        is_continuation: bool,
273        content_start: &mut Option<Span<'src>>,
274    ) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
275        // Optimization: If the first line doesn't match any of the early indications
276        // for delimited blocks, titles, or attrlists, we can skip directly to treating
277        // this as a simple block. That saves quite a bit of parsing time.
278        let first_line = source.take_line().item.discard_whitespace();
279
280        // If it does contain any of those markers, we fall through to the more costly
281        // tests below which can more accurately classify the upcoming block.
282        if let Some(first_char) = first_line.chars().next()
283            && !matches!(
284                first_char,
285                '.' | '#'
286                    | '='
287                    | '/'
288                    | '-'
289                    | '+'
290                    | '*'
291                    | '_'
292                    | '`'
293                    | '['
294                    | ':'
295                    | '\''
296                    | '<'
297                    | '>'
298                    | '"'
299                    | '•'
300            )
301            && !first_line.contains("::")
302            && !first_line.contains(";;")
303            && !TableBlock::is_table_delimiter(&first_line)
304            && !ListItemMarker::starts_with_marker(first_line)
305            && !starts_with_admonition_label(first_line)
306            && parent_list_markers.is_none()
307            && parser.pending_block_title.is_none()
308            && let Some(MatchedItem {
309                item: simple_block,
310                after,
311            }) = SimpleBlock::parse_fast(source, parser)
312        {
313            let mut warnings = vec![];
314            let block = Self::Simple(simple_block);
315
316            // This fast path only handles a metadata-free simple block, so there
317            // is no `[[id,reftext]]` anchor reftext to resolve.
318            Self::register_block_id(
319                block.id(),
320                Self::block_reftext(&block, None).as_deref(),
321                Self::block_signifier(&block, parser),
322                block.span(),
323                parser,
324                &mut warnings,
325            );
326
327            return MatchAndWarnings {
328                item: BlockParseOutcome::Parsed(MatchedItem { item: block, after }),
329                warnings,
330            };
331        }
332
333        // Look for document attributes first since these don't support block metadata.
334        if first_line.starts_with(':')
335            && (first_line.ends_with(':') || first_line.contains(": "))
336            && let Some(attr) = Attribute::parse(source, parser)
337        {
338            let mut warnings: Vec<Warning<'src>> = vec![];
339            parser.set_attribute_from_body(&attr.item, &mut warnings);
340
341            return MatchAndWarnings {
342                item: BlockParseOutcome::Parsed(MatchedItem {
343                    item: Self::DocumentAttribute(attr.item),
344                    after: attr.after,
345                }),
346                warnings,
347            };
348        }
349
350        // Optimization not possible; start by looking for block metadata (title,
351        // attrlist, etc.).
352        let MatchAndWarnings {
353            item: mut metadata,
354            mut warnings,
355        } = BlockMetadata::parse(source, parser);
356
357        // A block title stashed by an enclosing section heading (see
358        // `SectionBlock::parse`) is claimed by the next block parsed – this
359        // one. A title of the block's own wins, discarding the carried title.
360        // The carried title has no source line adjacent to this block, so
361        // `title_source` stays `None` (the same shape as a `title=` attribute).
362        if let Some(pending_title) = parser.pending_block_title.take()
363            && metadata.title.is_none()
364        {
365            // The carried title arrives as an owned snapshot; rebuild it as a
366            // `Content` anchored at the block's start, restoring any deferred
367            // cross-references so the title pass can still resolve them.
368            metadata.title = Some(crate::content::Content::from_owned_title(
369                metadata.block_start,
370                pending_title,
371            ));
372        }
373
374        // Tolerate a blank line between a block's metadata (title, anchor, or
375        // attribute list) and the block it decorates. Asciidoctor's
376        // `parse_block_metadata_lines` skips blank lines after each metadata
377        // line, so metadata separated from its block by one or more blank lines
378        // still attaches to that block rather than dangling as a spurious
379        // `MissingBlockAfterTitleOrAttributeList`. Advancing `block_start` past
380        // the gap lets the block-type dispatch below see the content directly.
381        //
382        // This applies at the block level only. Inside a list item,
383        // blank-separated metadata follows the list-continuation rules handled
384        // in `ListItem::parse` (where such metadata is discarded), so leave
385        // `block_start` pointing at the blank line for those callers. Likewise,
386        // if only blank lines follow (no block content), leave it untouched so
387        // the genuinely-dangling-metadata warning still fires.
388        if parent_list_markers.is_none() && !metadata.is_empty() {
389            let after_blanks = metadata.block_start.discard_empty_lines();
390            if after_blanks != metadata.block_start && !after_blanks.is_empty() {
391                metadata.block_start = after_blanks;
392            }
393        }
394
395        // Expose the content start (past the metadata) so `parse_internal` can
396        // anchor an unknown-style diagnostic at the block's delimiter or first
397        // content line. Every block-type dispatch below reads its content from
398        // here, so this is the block's true starting line whichever branch wins.
399        *content_start = Some(metadata.block_start);
400
401        // Resolve attribute references in a `[[id,reftext]]` anchor reftext now,
402        // while the parser still holds the attributes in effect where the anchor
403        // appears. A compound block's body (parsed below) can redefine those
404        // attributes, so deferring this to registration – after the body – would
405        // record the wrong value. The result is threaded into `block_reftext`.
406        let anchor_reftext = metadata
407            .anchor_reftext
408            .as_ref()
409            .map(|span| substitute_attributes_in_reftext(*span, parser));
410
411        // The `[literal]` block style normally marks a literal *paragraph*,
412        // which is handled directly as a simple (literal) block below, bypassing
413        // the delimited-block parsers. The exception is when `[literal]` is set
414        // on the delimiter line of a structural container, where it masquerades
415        // over that container (e.g. `[literal]` on a `----` listing, on a `....`
416        // literal, or on a `--` open block); those cases must fall through to the
417        // delimited-block parsers.
418        let is_literal =
419            metadata.attrlist.as_ref().and_then(|a| a.block_style()) == Some("literal") && {
420                let first_line = metadata.block_start.take_normalized_line().item;
421                !RawDelimitedBlock::is_valid_delimiter(&first_line)
422                    && !CompoundDelimitedBlock::is_valid_delimiter(&first_line)
423                    && !TableBlock::is_table_delimiter(&first_line)
424            };
425
426        // A simple block may be parsed speculatively inside the `!is_literal`
427        // branch below (to detect the "metadata with no block" edge case). When
428        // that speculative parse succeeds it is reused as the final result rather
429        // than re-parsed, so that the captioning side effect of
430        // `SimpleBlock::parse` (which can consume a caption counter) happens at
431        // most once per block.
432        let mut simple_block_mi = None;
433
434        if !is_literal {
435            if let Some(mut adm_maw) = AdmonitionBlock::parse(&metadata, parser)
436                && let Some(adm) = adm_maw.item
437            {
438                if !adm_maw.warnings.is_empty() {
439                    warnings.append(&mut adm_maw.warnings);
440                }
441
442                let block = Self::Admonition(adm.item);
443
444                Self::register_block_id(
445                    block.id(),
446                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
447                    Self::block_signifier(&block, parser),
448                    block.span(),
449                    parser,
450                    &mut warnings,
451                );
452
453                return MatchAndWarnings {
454                    item: BlockParseOutcome::Parsed(MatchedItem {
455                        item: block,
456                        after: adm.after,
457                    }),
458                    warnings,
459                };
460            }
461
462            if let Some(mut quote_maw) = QuoteBlock::parse(&metadata, parser)
463                && let Some(quote) = quote_maw.item
464            {
465                if !quote_maw.warnings.is_empty() {
466                    warnings.append(&mut quote_maw.warnings);
467                }
468
469                let block = Self::Quote(quote.item);
470
471                Self::register_block_id(
472                    block.id(),
473                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
474                    Self::block_signifier(&block, parser),
475                    block.span(),
476                    parser,
477                    &mut warnings,
478                );
479
480                return MatchAndWarnings {
481                    item: BlockParseOutcome::Parsed(MatchedItem {
482                        item: block,
483                        after: quote.after,
484                    }),
485                    warnings,
486                };
487            }
488
489            if let Some(mut rdb_maw) = RawDelimitedBlock::parse(&metadata, parser)
490                && let Some(rdb) = rdb_maw.item
491            {
492                if !rdb_maw.warnings.is_empty() {
493                    warnings.append(&mut rdb_maw.warnings);
494                }
495
496                let block = Self::RawDelimited(rdb.item);
497
498                Self::register_block_id(
499                    block.id(),
500                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
501                    Self::block_signifier(&block, parser),
502                    block.span(),
503                    parser,
504                    &mut warnings,
505                );
506
507                return MatchAndWarnings {
508                    item: BlockParseOutcome::Parsed(MatchedItem {
509                        item: block,
510                        after: rdb.after,
511                    }),
512                    warnings,
513                };
514            }
515
516            if let Some(mut cdb_maw) = CompoundDelimitedBlock::parse(&metadata, parser)
517                && let Some(cdb) = cdb_maw.item
518            {
519                if !cdb_maw.warnings.is_empty() {
520                    warnings.append(&mut cdb_maw.warnings);
521                }
522
523                let block = Self::CompoundDelimited(cdb.item);
524
525                Self::register_block_id(
526                    block.id(),
527                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
528                    Self::block_signifier(&block, parser),
529                    block.span(),
530                    parser,
531                    &mut warnings,
532                );
533
534                return MatchAndWarnings {
535                    item: BlockParseOutcome::Parsed(MatchedItem {
536                        item: block,
537                        after: cdb.after,
538                    }),
539                    warnings,
540                };
541            }
542
543            if let Some(mut table_maw) = TableBlock::parse(&metadata, parser)
544                && let Some(table) = table_maw.item
545            {
546                if !table_maw.warnings.is_empty() {
547                    warnings.append(&mut table_maw.warnings);
548                }
549
550                let block = Self::Table(table.item);
551
552                Self::register_block_id(
553                    block.id(),
554                    Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
555                    Self::block_signifier(&block, parser),
556                    block.span(),
557                    parser,
558                    &mut warnings,
559                );
560
561                return MatchAndWarnings {
562                    item: BlockParseOutcome::Parsed(MatchedItem {
563                        item: block,
564                        after: table.after,
565                    }),
566                    warnings,
567                };
568            }
569
570            // Try to discern the block type by scanning the first line.
571            let line = metadata.block_start.take_normalized_line();
572
573            if line.item.starts_with("image::")
574                || line.item.starts_with("video::")
575                || line.item.starts_with("audio::")
576            {
577                let mut media_block_maw = MediaBlock::parse(&metadata, parser);
578
579                if let Some(mut media_block) = media_block_maw.item {
580                    // Only propagate warnings from media block parsing if we think this
581                    // *is* a media block. Otherwise, there would likely be too many false
582                    // positives.
583                    if !media_block_maw.warnings.is_empty() {
584                        warnings.append(&mut media_block_maw.warnings);
585                    }
586
587                    // Resolve attribute references in the macro target. Under
588                    // `attribute-missing=drop-line`, a reference to a missing
589                    // attribute drops the entire block (Asciidoctor behavior).
590                    if media_block.item.resolve_target(parser) == TargetResolution::Drop {
591                        return MatchAndWarnings {
592                            item: BlockParseOutcome::Dropped(media_block.after),
593                            warnings,
594                        };
595                    }
596
597                    // Assign the caption only now that the block has survived
598                    // `resolve_target`, so a dropped image does not consume the
599                    // `figure-number` counter and leave a gap in the numbering.
600                    media_block.item.assign_caption(parser);
601
602                    let block = Self::Media(media_block.item);
603
604                    Self::register_block_id(
605                        block.id(),
606                        Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
607                        Self::block_signifier(&block, parser),
608                        block.span(),
609                        parser,
610                        &mut warnings,
611                    );
612
613                    return MatchAndWarnings {
614                        item: BlockParseOutcome::Parsed(MatchedItem {
615                            item: block,
616                            after: media_block.after,
617                        }),
618                        warnings,
619                    };
620                }
621
622                // This might be some other kind of block, so we don't
623                // automatically error out on a parse failure.
624            }
625
626            if line.item.starts_with("toc::") {
627                let mut toc_block_maw = TocBlock::parse(&metadata, parser);
628
629                if let Some(toc_block) = toc_block_maw.item {
630                    // Only propagate warnings from TOC block parsing if we think
631                    // this *is* a TOC block. Otherwise, there would likely be too
632                    // many false positives.
633                    if !toc_block_maw.warnings.is_empty() {
634                        warnings.append(&mut toc_block_maw.warnings);
635                    }
636
637                    let block = Self::Toc(toc_block.item);
638
639                    Self::register_block_id(
640                        block.id(),
641                        Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
642                        Self::block_signifier(&block, parser),
643                        block.span(),
644                        parser,
645                        &mut warnings,
646                    );
647
648                    return MatchAndWarnings {
649                        item: BlockParseOutcome::Parsed(MatchedItem {
650                            item: block,
651                            after: toc_block.after,
652                        }),
653                        warnings,
654                    };
655                }
656
657                // This might be some other kind of block, so we don't
658                // automatically error out on a parse failure.
659            }
660
661            if (line.item.starts_with('=') || line.item.starts_with('#'))
662                && let Some(mi_section_block) =
663                    SectionBlock::parse(&metadata, parser, &mut warnings)
664            {
665                // A line starting with `=` or `#` might be some other kind of block, so we
666                // continue quietly if `SectionBlock` parser rejects this block.
667
668                return MatchAndWarnings {
669                    item: BlockParseOutcome::Parsed(MatchedItem {
670                        item: Self::Section(mi_section_block.item),
671                        after: mi_section_block.after,
672                    }),
673                    warnings,
674                };
675            }
676
677            if (line.item.starts_with('\'')
678                || line.item.starts_with('-')
679                || line.item.starts_with('*')
680                || line.item.starts_with('_')
681                || line.item.starts_with('<'))
682                && let Some(mi_break) = Break::parse(&metadata, parser)
683            {
684                // Continue quietly if `Break` parser rejects this block.
685
686                return MatchAndWarnings {
687                    item: BlockParseOutcome::Parsed(MatchedItem {
688                        item: Self::Break(mi_break.item),
689                        after: mi_break.after,
690                    }),
691                    warnings,
692                };
693            }
694
695            // Only try to parse as a new list if we're NOT inside a list item context.
696            // If we are inside a list context, lists can only be created when the first
697            // line is a list item marker (handled above).
698            if parent_list_markers.is_none()
699                && let Some(mi_list) = ListBlock::parse(&metadata, parser, &mut warnings)
700            {
701                return MatchAndWarnings {
702                    item: BlockParseOutcome::Parsed(MatchedItem {
703                        item: Self::List(mi_list.item),
704                        after: mi_list.after,
705                    }),
706                    warnings,
707                };
708            }
709
710            // First, let's look for a fun edge case. Perhaps the text contains block
711            // metadata but no block immediately following. If we're not careful, we could
712            // spin in a loop (for example, `parse_blocks_until`) thinking there will be
713            // another block, but there isn't.
714
715            // The following check disables that spin loop.
716            simple_block_mi = if let Some(plm) = parent_list_markers {
717                SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
718            } else {
719                SimpleBlock::parse(&metadata, parser)
720            };
721
722            if simple_block_mi.is_none() {
723                if !metadata.is_empty() {
724                    // We have a metadata with no block. Treat it as a simple block but issue a
725                    // warning.
726
727                    warnings.push(Warning::new(
728                        metadata.source,
729                        WarningType::MissingBlockAfterTitleOrAttributeList,
730                    ));
731
732                    // Remove the metadata content so that SimpleBlock will read the title/attrlist
733                    // line(s) as regular content. The speculative parse failed, so the
734                    // block is re-parsed below with this stripped metadata.
735                    metadata.title_source = None;
736                    metadata.title = None;
737                    metadata.anchor = None;
738                    metadata.attrlist = None;
739                    metadata.block_start = metadata.source;
740                } else if !metadata.source.data().is_empty() {
741                    // The metadata scan consumed one or more do-nothing lines
742                    // (e.g. a lone empty `[[]]` anchor) that produced no title,
743                    // anchor, or attribute list, and no block follows them. The
744                    // lines are still consumed, so report the source as dropped
745                    // (resuming at `block_start`) rather than falling through to
746                    // `NoMatch`: a non-blank source left unadvanced would spin
747                    // the block-collection loop. Genuinely empty/blank input
748                    // (nothing consumed) still reaches `NoMatch` below.
749                    return MatchAndWarnings {
750                        item: BlockParseOutcome::Dropped(metadata.block_start),
751                        warnings,
752                    };
753                }
754            }
755        }
756
757        // If no other block kind matches, we can always use SimpleBlock. Reuse the
758        // speculative parse from the `!is_literal` branch when it succeeded;
759        // otherwise (a literal block, or metadata stripped above) parse now.
760        let simple_block_mi = match simple_block_mi {
761            Some(mi) => Some(mi),
762            None => {
763                if let Some(plm) = parent_list_markers {
764                    SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
765                } else {
766                    SimpleBlock::parse(&metadata, parser)
767                }
768            }
769        };
770
771        let mut result = MatchAndWarnings {
772            item: match simple_block_mi {
773                Some(mi) => BlockParseOutcome::Parsed(MatchedItem {
774                    item: Self::Simple(mi.item),
775                    after: mi.after,
776                }),
777                None => BlockParseOutcome::NoMatch,
778            },
779            warnings,
780        };
781
782        if let BlockParseOutcome::Parsed(ref matched_item) = result.item {
783            Self::register_block_id(
784                matched_item.item.id(),
785                Self::block_reftext(&matched_item.item, anchor_reftext.as_deref()).as_deref(),
786                Self::block_signifier(&matched_item.item, parser),
787                matched_item.item.span(),
788                parser,
789                &mut result.warnings,
790            );
791        }
792
793        result
794    }
795
796    /// Determine the [`XrefSignifier`] a cross-reference uses to build
797    /// `full`/`short` [`xrefstyle`](crate::parser::XrefStyle) text when this
798    /// block is the target.
799    ///
800    /// A signifier is produced only for an auto-numbered captioned block (e.g.
801    /// an image → "Figure 1", a titled table → "Table 1") that has no explicit
802    /// reftext. A block with an explicit `reftext` attribute or a
803    /// `[[id,reftext]]` anchor reftext uses that text verbatim, so it gets no
804    /// signifier; neither does an uncaptioned block or one whose caption was
805    /// overridden with `[caption=...]` (which is not numbered).
806    fn block_signifier<'a>(block: &'a Block<'a>, parser: &Parser) -> Option<XrefSignifier> {
807        // Only captioned blocks are eligible.
808        let caption = block.caption()?;
809
810        let has_explicit_reftext = block
811            .attrlist()
812            .and_then(|attrlist| attrlist.named_attribute("reftext"))
813            .is_some()
814            || block.anchor_reftext().is_some();
815        if has_explicit_reftext {
816            return None;
817        }
818
819        // Exclude explicit caption overrides, which are not numbered. This is
820        // *not* the same as `block.number().is_none()`: an auto-numbered block
821        // whose context counter holds a non-integer value (e.g. `:figure-number:
822        // A`, rendering "Figure B") also has no bare integer number, yet it is
823        // genuinely numbered and must keep its signifier ("Figure B").
824        if Self::has_caption_override(block, parser) {
825            return None;
826        }
827
828        // The caption prefix is "<label> <n>. "; the xrefstyle label is that
829        // prefix without its trailing ". " separator (e.g. "Figure 1").
830        let label = caption.strip_suffix(". ").unwrap_or(caption).to_string();
831        Some(XrefSignifier {
832            label,
833            emphasize: false,
834        })
835    }
836
837    /// Whether a captioned block's caption comes from an explicit override
838    /// rather than automatic numbering.
839    ///
840    /// An override is a `caption` attribute on the block (or, for an image, on
841    /// the image macro), or a non-empty document-wide `caption` attribute. This
842    /// mirrors the override detection in
843    /// [`caption::assign_block_caption`](crate::blocks::caption) and
844    /// [`MediaBlock::assign_caption`], so the two agree on which blocks are
845    /// numbered.
846    fn has_caption_override<'a>(block: &'a Block<'a>, parser: &Parser) -> bool {
847        let attribute_override = block
848            .attrlist()
849            .and_then(|attrlist| attrlist.named_attribute("caption"))
850            .is_some()
851            || matches!(block, Block::Media(media)
852                if media.macro_attrlist().named_attribute("caption").is_some());
853
854        attribute_override
855            || matches!(
856                parser.attribute_value("caption"),
857                InterpretedValue::Value(value) if !value.is_empty(),
858            )
859    }
860
861    /// Determine the reftext (a.k.a. xreflabel) used as the link text when a
862    /// block is the target of a cross reference. Asciidoctor's precedence is:
863    /// an explicit `reftext` attribute, then the reftext supplied with a
864    /// block anchor (`[[id,reftext]]`), and finally the block title.
865    ///
866    /// `anchor_reftext` is the block's `[[id,reftext]]` anchor reftext with its
867    /// attribute references already resolved (by the caller, against the
868    /// attributes in effect where the anchor appears – captured before the
869    /// block's body is parsed, since a compound block's body may itself
870    /// redefine those attributes). This matches how the anchor ID and a
871    /// `reftext=` attribute (both substituted when the attribute list is
872    /// parsed) are handled; the `reftext=` and title branches are already
873    /// substituted.
874    fn block_reftext<'a>(block: &'a Block<'a>, anchor_reftext: Option<&str>) -> Option<CowStr<'a>> {
875        if let Some(attr) = block
876            .attrlist()
877            .and_then(|attrlist| attrlist.named_attribute("reftext"))
878        {
879            return Some(CowStr::from(attr.value()));
880        }
881
882        if let Some(anchor_reftext) = anchor_reftext {
883            return Some(CowStr::from(anchor_reftext.to_string()));
884        }
885
886        block.title().map(CowStr::from)
887    }
888
889    /// Register a block's ID with the catalog if the block has an ID.
890    ///
891    /// This should be called for all block types except `SectionBlock`,
892    /// which handles its own catalog registration.
893    fn register_block_id(
894        id: Option<&str>,
895        reftext: Option<&str>,
896        signifier: Option<XrefSignifier>,
897        span: Span<'src>,
898        parser: &mut Parser,
899        warnings: &mut Vec<Warning<'src>>,
900    ) {
901        if let Some(id) = id {
902            match parser.register_ref(id, reftext, RefType::Anchor) {
903                Ok(()) => {
904                    if let Some(signifier) = signifier {
905                        parser.set_ref_signifier(id, signifier);
906                    }
907                }
908                Err(_duplicate_error) => {
909                    // If registration fails due to duplicate ID, issue a warning.
910                    warnings.push(Warning::new(span, WarningType::DuplicateId(id.to_string())));
911                }
912            }
913        }
914    }
915
916    /// Returns a reference to the inner [`ListItem`] if this is a
917    /// `Block::ListItem`, or `None` otherwise.
918    pub(crate) fn as_list_item(&self) -> Option<&ListItem<'src>> {
919        match self {
920            Self::ListItem(li) => Some(li),
921            _ => None,
922        }
923    }
924
925    /// Resolve any deferred cross-references in this block and its descendants,
926    /// using `resolver` to map targets to destinations and `renderer` to render
927    /// the resulting links. Unresolved targets are reported in `warnings`.
928    ///
929    /// This drives the recursion uniformly via the [`IsBlock::content_mut`] and
930    /// [`IsBlock::child_blocks_mut`] accessors, so it needs no per-block-type
931    /// special casing.
932    pub(crate) fn resolve_references(
933        &mut self,
934        resolver: &dyn ReferenceResolver,
935        renderer: &dyn InlineSubstitutionRenderer,
936        warnings: &mut ReferenceWarnings<'src>,
937    ) {
938        // A section is not resolved here: its resolvable content is its
939        // heading, which `content_mut` deliberately does not expose (see
940        // `SectionBlock`). Headings are resolved by the document-order title
941        // pass (`title_refs::resolve_title_references`), which coordinates
942        // cross-references *between* titles (forward and circular) – something
943        // per-content resolution cannot see.
944        if let Some(content) = self.content_mut() {
945            content.resolve_references(resolver, renderer, warnings);
946        }
947
948        // Tables hold their resolvable content in cells rather than in a single
949        // `content_mut()` value, so they are resolved explicitly here.
950        if let Self::Table(table) = self {
951            table.resolve_references(resolver, renderer, warnings);
952        }
953
954        // A Markdown-style blockquote holds its nested blocks in its own owned
955        // source, which the generic `child_blocks_mut()` walk below does not
956        // reach, so they are resolved explicitly here.
957        if let Self::Quote(quote) = self {
958            quote.resolve_references(resolver, renderer, warnings);
959        }
960
961        for child in self.child_blocks_mut() {
962            child.resolve_references(resolver, renderer, warnings);
963        }
964    }
965
966    /// Returns this block's *block title* (`.Title`) as a mutable [`Content`],
967    /// when the block has one.
968    ///
969    /// This is the decorative title carried above a block, distinct from a
970    /// section's heading. Used only by the document-order title resolution
971    /// pass, which reads a title's deferred cross-references and installs the
972    /// re-rendered title once they are resolved. Blocks that never carry a
973    /// title return `None`.
974    pub(crate) fn block_title_content_mut(&mut self) -> Option<&mut Content<'src>> {
975        match self {
976            Self::Simple(b) => b.title_content_mut(),
977            Self::Media(b) => b.title_content_mut(),
978            Self::List(b) => b.title_content_mut(),
979            Self::RawDelimited(b) => b.title_content_mut(),
980            Self::CompoundDelimited(b) => b.title_content_mut(),
981            Self::Admonition(b) => b.title_content_mut(),
982            Self::Quote(b) => b.title_content_mut(),
983            Self::Table(b) => b.title_content_mut(),
984            Self::Break(b) => b.title_content_mut(),
985            Self::Toc(b) => b.title_content_mut(),
986            _ => None,
987        }
988    }
989}
990
991impl<'src> IsBlock<'src> for Block<'src> {
992    fn content_model(&self) -> ContentModel {
993        match self {
994            Self::Simple(_) => ContentModel::Simple,
995            Self::Media(b) => b.content_model(),
996            Self::Section(_) => ContentModel::Compound,
997            Self::List(b) => b.content_model(),
998            Self::ListItem(b) => b.content_model(),
999            Self::RawDelimited(b) => b.content_model(),
1000            Self::CompoundDelimited(b) => b.content_model(),
1001            Self::Admonition(b) => b.content_model(),
1002            Self::Quote(b) => b.content_model(),
1003            Self::Table(b) => b.content_model(),
1004            Self::Preamble(b) => b.content_model(),
1005            Self::Break(b) => b.content_model(),
1006            Self::Toc(b) => b.content_model(),
1007            Self::DocumentAttribute(b) => b.content_model(),
1008        }
1009    }
1010
1011    fn declared_style(&'src self) -> Option<&'src str> {
1012        match self {
1013            Self::Simple(b) => b.declared_style(),
1014            Self::Media(b) => b.declared_style(),
1015            Self::Section(b) => b.declared_style(),
1016            Self::List(b) => b.declared_style(),
1017            Self::ListItem(b) => b.declared_style(),
1018            Self::RawDelimited(b) => b.declared_style(),
1019            Self::CompoundDelimited(b) => b.declared_style(),
1020            Self::Admonition(b) => b.declared_style(),
1021            Self::Quote(b) => b.declared_style(),
1022            Self::Table(b) => b.declared_style(),
1023            Self::Preamble(b) => b.declared_style(),
1024            Self::Break(b) => b.declared_style(),
1025            Self::Toc(b) => b.declared_style(),
1026            Self::DocumentAttribute(b) => b.declared_style(),
1027        }
1028    }
1029
1030    fn rendered_content(&'src self) -> Option<&'src str> {
1031        match self {
1032            Self::Simple(b) => b.rendered_content(),
1033            Self::Media(b) => b.rendered_content(),
1034            Self::Section(b) => b.rendered_content(),
1035            Self::List(b) => b.rendered_content(),
1036            Self::ListItem(b) => b.rendered_content(),
1037            Self::RawDelimited(b) => b.rendered_content(),
1038            Self::CompoundDelimited(b) => b.rendered_content(),
1039            Self::Admonition(b) => b.rendered_content(),
1040            Self::Quote(b) => b.rendered_content(),
1041            Self::Table(b) => b.rendered_content(),
1042            Self::Preamble(b) => b.rendered_content(),
1043            Self::Break(b) => b.rendered_content(),
1044            Self::Toc(b) => b.rendered_content(),
1045            Self::DocumentAttribute(b) => b.rendered_content(),
1046        }
1047    }
1048
1049    fn raw_context(&self) -> CowStr<'src> {
1050        match self {
1051            Self::Simple(b) => b.raw_context(),
1052            Self::Media(b) => b.raw_context(),
1053            Self::Section(b) => b.raw_context(),
1054            Self::List(b) => b.raw_context(),
1055            Self::ListItem(b) => b.raw_context(),
1056            Self::RawDelimited(b) => b.raw_context(),
1057            Self::CompoundDelimited(b) => b.raw_context(),
1058            Self::Admonition(b) => b.raw_context(),
1059            Self::Quote(b) => b.raw_context(),
1060            Self::Table(b) => b.raw_context(),
1061            Self::Preamble(b) => b.raw_context(),
1062            Self::Break(b) => b.raw_context(),
1063            Self::Toc(b) => b.raw_context(),
1064            Self::DocumentAttribute(b) => b.raw_context(),
1065        }
1066    }
1067
1068    fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
1069        match self {
1070            Self::Simple(b) => b.child_blocks_mut(),
1071            Self::Media(b) => b.child_blocks_mut(),
1072            Self::Section(b) => b.child_blocks_mut(),
1073            Self::List(b) => b.child_blocks_mut(),
1074            Self::ListItem(b) => b.child_blocks_mut(),
1075            Self::RawDelimited(b) => b.child_blocks_mut(),
1076            Self::CompoundDelimited(b) => b.child_blocks_mut(),
1077            Self::Admonition(b) => b.child_blocks_mut(),
1078            Self::Quote(b) => b.child_blocks_mut(),
1079            Self::Table(b) => b.child_blocks_mut(),
1080            Self::Preamble(b) => b.child_blocks_mut(),
1081            Self::Break(b) => b.child_blocks_mut(),
1082            Self::Toc(b) => b.child_blocks_mut(),
1083            Self::DocumentAttribute(b) => b.child_blocks_mut(),
1084        }
1085    }
1086
1087    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
1088        match self {
1089            Self::Simple(b) => b.content_mut(),
1090            Self::Media(b) => b.content_mut(),
1091            Self::Section(b) => b.content_mut(),
1092            Self::List(b) => b.content_mut(),
1093            Self::ListItem(b) => b.content_mut(),
1094            Self::RawDelimited(b) => b.content_mut(),
1095            Self::CompoundDelimited(b) => b.content_mut(),
1096            Self::Admonition(b) => b.content_mut(),
1097            Self::Quote(b) => b.content_mut(),
1098            Self::Table(b) => b.content_mut(),
1099            Self::Preamble(b) => b.content_mut(),
1100            Self::Break(b) => b.content_mut(),
1101            Self::Toc(b) => b.content_mut(),
1102            Self::DocumentAttribute(b) => b.content_mut(),
1103        }
1104    }
1105
1106    fn title_source(&'src self) -> Option<Span<'src>> {
1107        match self {
1108            Self::Simple(b) => b.title_source(),
1109            Self::Media(b) => b.title_source(),
1110            Self::Section(b) => b.title_source(),
1111            Self::List(b) => b.title_source(),
1112            Self::ListItem(b) => b.title_source(),
1113            Self::RawDelimited(b) => b.title_source(),
1114            Self::CompoundDelimited(b) => b.title_source(),
1115            Self::Admonition(b) => b.title_source(),
1116            Self::Quote(b) => b.title_source(),
1117            Self::Table(b) => b.title_source(),
1118            Self::Preamble(b) => b.title_source(),
1119            Self::Break(b) => b.title_source(),
1120            Self::Toc(b) => b.title_source(),
1121            Self::DocumentAttribute(b) => b.title_source(),
1122        }
1123    }
1124
1125    fn title(&self) -> Option<&str> {
1126        match self {
1127            Self::Simple(b) => b.title(),
1128            Self::Media(b) => b.title(),
1129            Self::Section(b) => b.title(),
1130            Self::List(b) => b.title(),
1131            Self::ListItem(b) => b.title(),
1132            Self::RawDelimited(b) => b.title(),
1133            Self::CompoundDelimited(b) => b.title(),
1134            Self::Admonition(b) => b.title(),
1135            Self::Quote(b) => b.title(),
1136            Self::Table(b) => b.title(),
1137            Self::Preamble(b) => b.title(),
1138            Self::Break(b) => b.title(),
1139            Self::Toc(b) => b.title(),
1140            Self::DocumentAttribute(b) => b.title(),
1141        }
1142    }
1143
1144    fn caption(&self) -> Option<&str> {
1145        match self {
1146            Self::Simple(b) => b.caption(),
1147            Self::Media(b) => b.caption(),
1148            Self::Section(b) => b.caption(),
1149            Self::List(b) => b.caption(),
1150            Self::ListItem(b) => b.caption(),
1151            Self::RawDelimited(b) => b.caption(),
1152            Self::CompoundDelimited(b) => b.caption(),
1153            Self::Admonition(b) => b.caption(),
1154            Self::Quote(b) => b.caption(),
1155            Self::Table(b) => b.caption(),
1156            Self::Preamble(b) => b.caption(),
1157            Self::Break(b) => b.caption(),
1158            Self::Toc(b) => b.caption(),
1159            Self::DocumentAttribute(b) => b.caption(),
1160        }
1161    }
1162
1163    fn number(&self) -> Option<usize> {
1164        match self {
1165            Self::Simple(b) => b.number(),
1166            Self::Media(b) => b.number(),
1167            Self::Section(b) => b.number(),
1168            Self::List(b) => b.number(),
1169            Self::ListItem(b) => b.number(),
1170            Self::RawDelimited(b) => b.number(),
1171            Self::CompoundDelimited(b) => b.number(),
1172            Self::Admonition(b) => b.number(),
1173            Self::Quote(b) => b.number(),
1174            Self::Table(b) => b.number(),
1175            Self::Preamble(b) => b.number(),
1176            Self::Break(b) => b.number(),
1177            Self::Toc(b) => b.number(),
1178            Self::DocumentAttribute(b) => b.number(),
1179        }
1180    }
1181
1182    fn id(&'src self) -> Option<&'src str> {
1183        // Three variants override the trait default:
1184        //
1185        // * A `MediaBlock` additionally recognizes a named `id=` _inside_ its macro
1186        //   attribute list (e.g. `image::sunset.jpg[id=sunset-img]`).
1187        //
1188        // * A `TocBlock` likewise recognizes a named `id=` _inside_ its macro attribute
1189        //   list (e.g. `toc::[id=contents]`).
1190        //
1191        // * A `SectionBlock` falls back to its auto-generated (`_slug`) ID when no
1192        //   explicit ID was supplied, so `block.id()` yields the same ID the section is
1193        //   registered and cross-referenced under. Delegating here (rather than
1194        //   applying the trait default) avoids the footgun of `block.id()` silently
1195        //   returning `None` for a section that plainly has an ID.
1196        //
1197        // Every other variant keeps the trait default (explicit anchor or block
1198        // attribute list only).
1199        match self {
1200            Self::Media(b) => b.id(),
1201            Self::Section(b) => b.id(),
1202            Self::Toc(b) => b.id(),
1203            _ => self
1204                .anchor()
1205                .map(|a| a.data())
1206                .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id())),
1207        }
1208    }
1209
1210    fn anchor(&'src self) -> Option<Span<'src>> {
1211        match self {
1212            Self::Simple(b) => b.anchor(),
1213            Self::Media(b) => b.anchor(),
1214            Self::Section(b) => b.anchor(),
1215            Self::List(b) => b.anchor(),
1216            Self::ListItem(b) => b.anchor(),
1217            Self::RawDelimited(b) => b.anchor(),
1218            Self::CompoundDelimited(b) => b.anchor(),
1219            Self::Admonition(b) => b.anchor(),
1220            Self::Quote(b) => b.anchor(),
1221            Self::Table(b) => b.anchor(),
1222            Self::Preamble(b) => b.anchor(),
1223            Self::Break(b) => b.anchor(),
1224            Self::Toc(b) => b.anchor(),
1225            Self::DocumentAttribute(b) => b.anchor(),
1226        }
1227    }
1228
1229    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
1230        match self {
1231            Self::Simple(b) => b.anchor_reftext(),
1232            Self::Media(b) => b.anchor_reftext(),
1233            Self::Section(b) => b.anchor_reftext(),
1234            Self::List(b) => b.anchor_reftext(),
1235            Self::ListItem(b) => b.anchor_reftext(),
1236            Self::RawDelimited(b) => b.anchor_reftext(),
1237            Self::CompoundDelimited(b) => b.anchor_reftext(),
1238            Self::Admonition(b) => b.anchor_reftext(),
1239            Self::Quote(b) => b.anchor_reftext(),
1240            Self::Table(b) => b.anchor_reftext(),
1241            Self::Preamble(b) => b.anchor_reftext(),
1242            Self::Break(b) => b.anchor_reftext(),
1243            Self::Toc(b) => b.anchor_reftext(),
1244            Self::DocumentAttribute(b) => b.anchor_reftext(),
1245        }
1246    }
1247
1248    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
1249        match self {
1250            Self::Simple(b) => b.attrlist(),
1251            Self::Media(b) => b.attrlist(),
1252            Self::Section(b) => b.attrlist(),
1253            Self::List(b) => b.attrlist(),
1254            Self::ListItem(b) => b.attrlist(),
1255            Self::RawDelimited(b) => b.attrlist(),
1256            Self::CompoundDelimited(b) => b.attrlist(),
1257            Self::Admonition(b) => b.attrlist(),
1258            Self::Quote(b) => b.attrlist(),
1259            Self::Table(b) => b.attrlist(),
1260            Self::Preamble(b) => b.attrlist(),
1261            Self::Break(b) => b.attrlist(),
1262            Self::Toc(b) => b.attrlist(),
1263            Self::DocumentAttribute(b) => b.attrlist(),
1264        }
1265    }
1266
1267    fn substitution_group(&self) -> SubstitutionGroup {
1268        match self {
1269            Self::Simple(b) => b.substitution_group(),
1270            Self::Media(b) => b.substitution_group(),
1271            Self::Section(b) => b.substitution_group(),
1272            Self::List(b) => b.substitution_group(),
1273            Self::ListItem(b) => b.substitution_group(),
1274            Self::RawDelimited(b) => b.substitution_group(),
1275            Self::CompoundDelimited(b) => b.substitution_group(),
1276            Self::Admonition(b) => b.substitution_group(),
1277            Self::Quote(b) => b.substitution_group(),
1278            Self::Table(b) => b.substitution_group(),
1279            Self::Preamble(b) => b.substitution_group(),
1280            Self::Break(b) => b.substitution_group(),
1281            Self::Toc(b) => b.substitution_group(),
1282            Self::DocumentAttribute(b) => b.substitution_group(),
1283        }
1284    }
1285}
1286
1287impl<'src> HasSpan<'src> for Block<'src> {
1288    fn span(&self) -> Span<'src> {
1289        match self {
1290            Self::Simple(b) => b.span(),
1291            Self::Media(b) => b.span(),
1292            Self::Section(b) => b.span(),
1293            Self::List(b) => b.span(),
1294            Self::ListItem(b) => b.span(),
1295            Self::RawDelimited(b) => b.span(),
1296            Self::CompoundDelimited(b) => b.span(),
1297            Self::Admonition(b) => b.span(),
1298            Self::Quote(b) => b.span(),
1299            Self::Table(b) => b.span(),
1300            Self::Preamble(b) => b.span(),
1301            Self::Break(b) => b.span(),
1302            Self::Toc(b) => b.span(),
1303            Self::DocumentAttribute(b) => b.span(),
1304        }
1305    }
1306}
1307
1308/// The five admonition labels (`NOTE`, `TIP`, …). A style naming one of these
1309/// is always recognized: it turns the block into an admonition.
1310const ADMONITION_STYLES: &[&str] = &["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"];
1311
1312/// Block style keywords this parser understands that are *not* themselves
1313/// built-in contexts.
1314///
1315/// A declared block style (the first positional attribute, e.g. `[source]`) is
1316/// recognized when it names a built-in context this parser can adopt (any
1317/// [`is_built_in_context`] style – `example`, `listing`, `sidebar`, `image`,
1318/// `table`, and so on), one of these interpreting keywords, or – via
1319/// [`ADMONITION_STYLES`] – an admonition label. Anything else is an unknown
1320/// style.
1321///
1322/// Deferring the built-in contexts to [`is_built_in_context`] keeps this check
1323/// in lock-step with
1324/// [`resolved_context`](crate::blocks::IsBlock::resolved_context), which adopts
1325/// exactly those styles as the block's context: a style that
1326/// `resolved_context` acts on is never reported as unknown. These keywords are
1327/// the remaining styles this parser interprets without their being a context –
1328/// `source` specializes `listing`, `abstract` an open block, the `stem`
1329/// flavors a `stem` block, and `comment`/`normal`/`partintro` round out
1330/// Asciidoctor's `PARAGRAPH_STYLES`.
1331const KNOWN_STYLE_KEYWORDS: &[&str] = &[
1332    "abstract",
1333    "asciimath",
1334    "comment",
1335    "latexmath",
1336    "normal",
1337    "partintro",
1338    "source",
1339];
1340
1341/// The block contexts for which an unknown declared style is diagnosed.
1342///
1343/// Asciidoctor reports an unknown style only for delimited blocks and
1344/// paragraphs; a style on a list, section, media macro, or thematic break is
1345/// interpreted differently (a list marker style, a discrete-heading style,
1346/// etc.) and is never reported this way. These are the corresponding
1347/// [`raw_context`](crate::blocks::IsBlock::raw_context) values.
1348///
1349/// [`raw_context`]: crate::blocks::IsBlock::raw_context
1350const STYLED_BLOCK_CONTEXTS: &[&str] = &[
1351    "comment",
1352    "example",
1353    "listing",
1354    "literal",
1355    "open",
1356    "paragraph",
1357    "pass",
1358    "quote",
1359    "sidebar",
1360    "stem",
1361    "table",
1362    "verse",
1363];
1364
1365/// Diagnose a block that declared a style this parser does not recognize for
1366/// its context, returning the [`WarningType`] to record or `None` when the
1367/// block has no style, the style is recognized, or the block's context is not
1368/// one for which an unknown style is reported.
1369///
1370/// The block keeps the default context implied by its syntax regardless; the
1371/// diagnostic only reports that the declared style had no effect. This mirrors
1372/// Asciidoctor, which logs `unknown style for <context> block: <style>` at
1373/// debug level and falls back to the block's context.
1374fn unknown_block_style_warning(block: &Block<'_>) -> Option<WarningType> {
1375    let style = block.declared_style()?;
1376    let context = block.raw_context();
1377    let context = context.as_ref();
1378
1379    // Only diagnose something that actually looks like a style name. A malformed
1380    // attribute list (for example a mangled `[[anchor]`, or a positional whose
1381    // value is `-foo = bar`) can leave debris in the first positional slot that
1382    // is not a plausible style; reporting it as an "unknown style" would be
1383    // noise. Asciidoctor only reaches this check with a properly-parsed style
1384    // token, so a value carrying spaces, brackets, or other punctuation is not
1385    // one this diagnostic is meant for.
1386    if !is_plausible_style_name(style) {
1387        return None;
1388    }
1389
1390    // Only delimited blocks and paragraphs report an unknown style; on any
1391    // other block a first positional attribute means something else.
1392    if !STYLED_BLOCK_CONTEXTS.contains(&context) {
1393        return None;
1394    }
1395
1396    if is_built_in_context(style)
1397        || KNOWN_STYLE_KEYWORDS.contains(&style)
1398        || ADMONITION_STYLES.contains(&style)
1399    {
1400        return None;
1401    }
1402
1403    Some(WarningType::UnknownBlockStyle(
1404        context.to_string(),
1405        style.to_string(),
1406    ))
1407}
1408
1409/// Whether `style` looks like an authored block-style name – a non-empty token
1410/// of ASCII letters, digits, `_`, or `-`. This screens out the debris a
1411/// malformed attribute list can leave in the first positional slot (values
1412/// carrying spaces, brackets, `=`, quotes, and so on), which is not a style the
1413/// unknown-style diagnostic is meant to report.
1414fn is_plausible_style_name(style: &str) -> bool {
1415    !style.is_empty()
1416        && style
1417            .chars()
1418            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    #![allow(clippy::unwrap_used)]
1424
1425    mod unknown_block_style {
1426        use crate::{
1427            Parser,
1428            warnings::{WarningSeverity, WarningType},
1429        };
1430
1431        /// The single warning a parse produced, or `None` if there were none.
1432        fn only_warning(input: &str) -> Option<(WarningSeverity, WarningType)> {
1433            let doc = Parser::default().parse(input);
1434            let mut warnings = doc.warnings();
1435            let warning = warnings.next()?;
1436            assert!(
1437                warnings.next().is_none(),
1438                "expected at most one warning for {input:?}"
1439            );
1440
1441            Some((warning.severity, warning.warning.clone()))
1442        }
1443
1444        #[test]
1445        fn unknown_style_on_open_block_is_debug() {
1446            assert_eq!(
1447                only_warning("[foo]\n--\nbar\n--\n"),
1448                Some((
1449                    WarningSeverity::Debug,
1450                    WarningType::UnknownBlockStyle("open".to_string(), "foo".to_string())
1451                ))
1452            );
1453        }
1454
1455        #[test]
1456        fn unknown_style_on_paragraph_is_debug() {
1457            assert_eq!(
1458                only_warning("[foo]\nbar\n"),
1459                Some((
1460                    WarningSeverity::Debug,
1461                    WarningType::UnknownBlockStyle("paragraph".to_string(), "foo".to_string())
1462                ))
1463            );
1464        }
1465
1466        #[test]
1467        fn nested_unknown_style_is_reported() {
1468            assert_eq!(
1469                only_warning("====\n[bar]\n--\nx\n--\n====\n"),
1470                Some((
1471                    WarningSeverity::Debug,
1472                    WarningType::UnknownBlockStyle("open".to_string(), "bar".to_string())
1473                ))
1474            );
1475        }
1476
1477        #[test]
1478        fn recognized_context_style_does_not_warn() {
1479            // A built-in context masquerading over a delimited block.
1480            assert_eq!(only_warning("[example]\n--\nx\n--\n"), None);
1481            assert_eq!(only_warning("[sidebar]\n--\nx\n--\n"), None);
1482        }
1483
1484        #[test]
1485        fn recognized_keyword_style_does_not_warn() {
1486            assert_eq!(only_warning("[source]\n----\nx\n----\n"), None);
1487            assert_eq!(only_warning("[verse]\n____\nx\n____\n"), None);
1488            assert_eq!(only_warning("[abstract]\n--\nx\n--\n"), None);
1489            assert_eq!(only_warning("[asciimath]\n++++\nx\n++++\n"), None);
1490        }
1491
1492        #[test]
1493        fn any_built_in_context_style_does_not_warn() {
1494            // Every built-in context this parser can adopt as a block's context
1495            // (via `resolved_context`) is a recognized style, even one it does
1496            // not otherwise treat as a masquerade keyword (e.g. `image`,
1497            // `audio`, `video`, `table`). Reporting these would contradict the
1498            // context the parser actually resolved.
1499            assert_eq!(only_warning("[image]\nbar\n"), None);
1500            assert_eq!(only_warning("[audio]\n--\nx\n--\n"), None);
1501            assert_eq!(only_warning("[video]\nbar\n"), None);
1502            assert_eq!(only_warning("[table]\nbar\n"), None);
1503        }
1504
1505        #[test]
1506        fn admonition_style_does_not_warn() {
1507            assert_eq!(only_warning("[NOTE]\n--\nx\n--\n"), None);
1508        }
1509
1510        #[test]
1511        fn style_naming_its_own_context_does_not_warn() {
1512            // A `[table]` style on a table names the block's own context.
1513            assert_eq!(only_warning("[table]\n|===\n| x\n|===\n"), None);
1514        }
1515
1516        #[test]
1517        fn style_on_list_or_section_does_not_warn() {
1518            // A first positional on a list or a section heading is not a block
1519            // style (a list marker style, a discrete-heading flag, and so on).
1520            assert_eq!(only_warning("[square]\n* a\n* b\n"), None);
1521            assert_eq!(only_warning("[discrete]\n== Heading\n"), None);
1522        }
1523
1524        #[test]
1525        fn malformed_attrlist_debris_does_not_warn() {
1526            // A mangled anchor (`[[notice]`) leaves `[notice` in the first
1527            // positional slot; it is not a plausible style name.
1528            assert_eq!(only_warning("[[notice]\nThis is a paragraph.\n"), None);
1529        }
1530    }
1531
1532    mod is_plausible_style_name {
1533        use crate::blocks::block::is_plausible_style_name;
1534
1535        #[test]
1536        fn accepts_style_tokens() {
1537            assert!(is_plausible_style_name("foo"));
1538            assert!(is_plausible_style_name("NOTE"));
1539            assert!(is_plausible_style_name("foo-bar"));
1540            assert!(is_plausible_style_name("foo_bar"));
1541            assert!(is_plausible_style_name("style2"));
1542        }
1543
1544        #[test]
1545        fn rejects_debris_and_empty() {
1546            assert!(!is_plausible_style_name(""));
1547            assert!(!is_plausible_style_name("[notice"));
1548            assert!(!is_plausible_style_name("-foo = bar"));
1549            assert!(!is_plausible_style_name("a,b"));
1550            assert!(!is_plausible_style_name("has space"));
1551        }
1552    }
1553}