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}