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 resolved_style(&'src self) -> Option<&'src str> {
1031 match self {
1032 Self::Simple(b) => b.resolved_style(),
1033 Self::Media(b) => b.resolved_style(),
1034 Self::Section(b) => b.resolved_style(),
1035 Self::List(b) => b.resolved_style(),
1036 Self::ListItem(b) => b.resolved_style(),
1037 Self::RawDelimited(b) => b.resolved_style(),
1038 Self::CompoundDelimited(b) => b.resolved_style(),
1039 Self::Admonition(b) => b.resolved_style(),
1040 Self::Quote(b) => b.resolved_style(),
1041 Self::Table(b) => b.resolved_style(),
1042 Self::Preamble(b) => b.resolved_style(),
1043 Self::Break(b) => b.resolved_style(),
1044 Self::Toc(b) => b.resolved_style(),
1045 Self::DocumentAttribute(b) => b.resolved_style(),
1046 }
1047 }
1048
1049 fn rendered_content(&'src self) -> Option<&'src str> {
1050 match self {
1051 Self::Simple(b) => b.rendered_content(),
1052 Self::Media(b) => b.rendered_content(),
1053 Self::Section(b) => b.rendered_content(),
1054 Self::List(b) => b.rendered_content(),
1055 Self::ListItem(b) => b.rendered_content(),
1056 Self::RawDelimited(b) => b.rendered_content(),
1057 Self::CompoundDelimited(b) => b.rendered_content(),
1058 Self::Admonition(b) => b.rendered_content(),
1059 Self::Quote(b) => b.rendered_content(),
1060 Self::Table(b) => b.rendered_content(),
1061 Self::Preamble(b) => b.rendered_content(),
1062 Self::Break(b) => b.rendered_content(),
1063 Self::Toc(b) => b.rendered_content(),
1064 Self::DocumentAttribute(b) => b.rendered_content(),
1065 }
1066 }
1067
1068 fn raw_context(&self) -> CowStr<'src> {
1069 match self {
1070 Self::Simple(b) => b.raw_context(),
1071 Self::Media(b) => b.raw_context(),
1072 Self::Section(b) => b.raw_context(),
1073 Self::List(b) => b.raw_context(),
1074 Self::ListItem(b) => b.raw_context(),
1075 Self::RawDelimited(b) => b.raw_context(),
1076 Self::CompoundDelimited(b) => b.raw_context(),
1077 Self::Admonition(b) => b.raw_context(),
1078 Self::Quote(b) => b.raw_context(),
1079 Self::Table(b) => b.raw_context(),
1080 Self::Preamble(b) => b.raw_context(),
1081 Self::Break(b) => b.raw_context(),
1082 Self::Toc(b) => b.raw_context(),
1083 Self::DocumentAttribute(b) => b.raw_context(),
1084 }
1085 }
1086
1087 fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
1088 match self {
1089 Self::Simple(b) => b.child_blocks_mut(),
1090 Self::Media(b) => b.child_blocks_mut(),
1091 Self::Section(b) => b.child_blocks_mut(),
1092 Self::List(b) => b.child_blocks_mut(),
1093 Self::ListItem(b) => b.child_blocks_mut(),
1094 Self::RawDelimited(b) => b.child_blocks_mut(),
1095 Self::CompoundDelimited(b) => b.child_blocks_mut(),
1096 Self::Admonition(b) => b.child_blocks_mut(),
1097 Self::Quote(b) => b.child_blocks_mut(),
1098 Self::Table(b) => b.child_blocks_mut(),
1099 Self::Preamble(b) => b.child_blocks_mut(),
1100 Self::Break(b) => b.child_blocks_mut(),
1101 Self::Toc(b) => b.child_blocks_mut(),
1102 Self::DocumentAttribute(b) => b.child_blocks_mut(),
1103 }
1104 }
1105
1106 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
1107 match self {
1108 Self::Simple(b) => b.content_mut(),
1109 Self::Media(b) => b.content_mut(),
1110 Self::Section(b) => b.content_mut(),
1111 Self::List(b) => b.content_mut(),
1112 Self::ListItem(b) => b.content_mut(),
1113 Self::RawDelimited(b) => b.content_mut(),
1114 Self::CompoundDelimited(b) => b.content_mut(),
1115 Self::Admonition(b) => b.content_mut(),
1116 Self::Quote(b) => b.content_mut(),
1117 Self::Table(b) => b.content_mut(),
1118 Self::Preamble(b) => b.content_mut(),
1119 Self::Break(b) => b.content_mut(),
1120 Self::Toc(b) => b.content_mut(),
1121 Self::DocumentAttribute(b) => b.content_mut(),
1122 }
1123 }
1124
1125 fn title_source(&'src self) -> Option<Span<'src>> {
1126 match self {
1127 Self::Simple(b) => b.title_source(),
1128 Self::Media(b) => b.title_source(),
1129 Self::Section(b) => b.title_source(),
1130 Self::List(b) => b.title_source(),
1131 Self::ListItem(b) => b.title_source(),
1132 Self::RawDelimited(b) => b.title_source(),
1133 Self::CompoundDelimited(b) => b.title_source(),
1134 Self::Admonition(b) => b.title_source(),
1135 Self::Quote(b) => b.title_source(),
1136 Self::Table(b) => b.title_source(),
1137 Self::Preamble(b) => b.title_source(),
1138 Self::Break(b) => b.title_source(),
1139 Self::Toc(b) => b.title_source(),
1140 Self::DocumentAttribute(b) => b.title_source(),
1141 }
1142 }
1143
1144 fn title(&self) -> Option<&str> {
1145 match self {
1146 Self::Simple(b) => b.title(),
1147 Self::Media(b) => b.title(),
1148 Self::Section(b) => b.title(),
1149 Self::List(b) => b.title(),
1150 Self::ListItem(b) => b.title(),
1151 Self::RawDelimited(b) => b.title(),
1152 Self::CompoundDelimited(b) => b.title(),
1153 Self::Admonition(b) => b.title(),
1154 Self::Quote(b) => b.title(),
1155 Self::Table(b) => b.title(),
1156 Self::Preamble(b) => b.title(),
1157 Self::Break(b) => b.title(),
1158 Self::Toc(b) => b.title(),
1159 Self::DocumentAttribute(b) => b.title(),
1160 }
1161 }
1162
1163 fn caption(&self) -> Option<&str> {
1164 match self {
1165 Self::Simple(b) => b.caption(),
1166 Self::Media(b) => b.caption(),
1167 Self::Section(b) => b.caption(),
1168 Self::List(b) => b.caption(),
1169 Self::ListItem(b) => b.caption(),
1170 Self::RawDelimited(b) => b.caption(),
1171 Self::CompoundDelimited(b) => b.caption(),
1172 Self::Admonition(b) => b.caption(),
1173 Self::Quote(b) => b.caption(),
1174 Self::Table(b) => b.caption(),
1175 Self::Preamble(b) => b.caption(),
1176 Self::Break(b) => b.caption(),
1177 Self::Toc(b) => b.caption(),
1178 Self::DocumentAttribute(b) => b.caption(),
1179 }
1180 }
1181
1182 fn number(&self) -> Option<usize> {
1183 match self {
1184 Self::Simple(b) => b.number(),
1185 Self::Media(b) => b.number(),
1186 Self::Section(b) => b.number(),
1187 Self::List(b) => b.number(),
1188 Self::ListItem(b) => b.number(),
1189 Self::RawDelimited(b) => b.number(),
1190 Self::CompoundDelimited(b) => b.number(),
1191 Self::Admonition(b) => b.number(),
1192 Self::Quote(b) => b.number(),
1193 Self::Table(b) => b.number(),
1194 Self::Preamble(b) => b.number(),
1195 Self::Break(b) => b.number(),
1196 Self::Toc(b) => b.number(),
1197 Self::DocumentAttribute(b) => b.number(),
1198 }
1199 }
1200
1201 fn id(&'src self) -> Option<&'src str> {
1202 // Three variants override the trait default:
1203 //
1204 // * A `MediaBlock` additionally recognizes a named `id=` _inside_ its macro
1205 // attribute list (e.g. `image::sunset.jpg[id=sunset-img]`).
1206 //
1207 // * A `TocBlock` likewise recognizes a named `id=` _inside_ its macro attribute
1208 // list (e.g. `toc::[id=contents]`).
1209 //
1210 // * A `SectionBlock` falls back to its auto-generated (`_slug`) ID when no
1211 // explicit ID was supplied, so `block.id()` yields the same ID the section is
1212 // registered and cross-referenced under. Delegating here (rather than
1213 // applying the trait default) avoids the footgun of `block.id()` silently
1214 // returning `None` for a section that plainly has an ID.
1215 //
1216 // Every other variant keeps the trait default (explicit anchor or block
1217 // attribute list only).
1218 match self {
1219 Self::Media(b) => b.id(),
1220 Self::Section(b) => b.id(),
1221 Self::Toc(b) => b.id(),
1222 _ => self
1223 .anchor()
1224 .map(|a| a.data())
1225 .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id())),
1226 }
1227 }
1228
1229 fn anchor(&'src self) -> Option<Span<'src>> {
1230 match self {
1231 Self::Simple(b) => b.anchor(),
1232 Self::Media(b) => b.anchor(),
1233 Self::Section(b) => b.anchor(),
1234 Self::List(b) => b.anchor(),
1235 Self::ListItem(b) => b.anchor(),
1236 Self::RawDelimited(b) => b.anchor(),
1237 Self::CompoundDelimited(b) => b.anchor(),
1238 Self::Admonition(b) => b.anchor(),
1239 Self::Quote(b) => b.anchor(),
1240 Self::Table(b) => b.anchor(),
1241 Self::Preamble(b) => b.anchor(),
1242 Self::Break(b) => b.anchor(),
1243 Self::Toc(b) => b.anchor(),
1244 Self::DocumentAttribute(b) => b.anchor(),
1245 }
1246 }
1247
1248 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
1249 match self {
1250 Self::Simple(b) => b.anchor_reftext(),
1251 Self::Media(b) => b.anchor_reftext(),
1252 Self::Section(b) => b.anchor_reftext(),
1253 Self::List(b) => b.anchor_reftext(),
1254 Self::ListItem(b) => b.anchor_reftext(),
1255 Self::RawDelimited(b) => b.anchor_reftext(),
1256 Self::CompoundDelimited(b) => b.anchor_reftext(),
1257 Self::Admonition(b) => b.anchor_reftext(),
1258 Self::Quote(b) => b.anchor_reftext(),
1259 Self::Table(b) => b.anchor_reftext(),
1260 Self::Preamble(b) => b.anchor_reftext(),
1261 Self::Break(b) => b.anchor_reftext(),
1262 Self::Toc(b) => b.anchor_reftext(),
1263 Self::DocumentAttribute(b) => b.anchor_reftext(),
1264 }
1265 }
1266
1267 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
1268 match self {
1269 Self::Simple(b) => b.attrlist(),
1270 Self::Media(b) => b.attrlist(),
1271 Self::Section(b) => b.attrlist(),
1272 Self::List(b) => b.attrlist(),
1273 Self::ListItem(b) => b.attrlist(),
1274 Self::RawDelimited(b) => b.attrlist(),
1275 Self::CompoundDelimited(b) => b.attrlist(),
1276 Self::Admonition(b) => b.attrlist(),
1277 Self::Quote(b) => b.attrlist(),
1278 Self::Table(b) => b.attrlist(),
1279 Self::Preamble(b) => b.attrlist(),
1280 Self::Break(b) => b.attrlist(),
1281 Self::Toc(b) => b.attrlist(),
1282 Self::DocumentAttribute(b) => b.attrlist(),
1283 }
1284 }
1285
1286 fn substitution_group(&self) -> SubstitutionGroup {
1287 match self {
1288 Self::Simple(b) => b.substitution_group(),
1289 Self::Media(b) => b.substitution_group(),
1290 Self::Section(b) => b.substitution_group(),
1291 Self::List(b) => b.substitution_group(),
1292 Self::ListItem(b) => b.substitution_group(),
1293 Self::RawDelimited(b) => b.substitution_group(),
1294 Self::CompoundDelimited(b) => b.substitution_group(),
1295 Self::Admonition(b) => b.substitution_group(),
1296 Self::Quote(b) => b.substitution_group(),
1297 Self::Table(b) => b.substitution_group(),
1298 Self::Preamble(b) => b.substitution_group(),
1299 Self::Break(b) => b.substitution_group(),
1300 Self::Toc(b) => b.substitution_group(),
1301 Self::DocumentAttribute(b) => b.substitution_group(),
1302 }
1303 }
1304}
1305
1306impl<'src> HasSpan<'src> for Block<'src> {
1307 fn span(&self) -> Span<'src> {
1308 match self {
1309 Self::Simple(b) => b.span(),
1310 Self::Media(b) => b.span(),
1311 Self::Section(b) => b.span(),
1312 Self::List(b) => b.span(),
1313 Self::ListItem(b) => b.span(),
1314 Self::RawDelimited(b) => b.span(),
1315 Self::CompoundDelimited(b) => b.span(),
1316 Self::Admonition(b) => b.span(),
1317 Self::Quote(b) => b.span(),
1318 Self::Table(b) => b.span(),
1319 Self::Preamble(b) => b.span(),
1320 Self::Break(b) => b.span(),
1321 Self::Toc(b) => b.span(),
1322 Self::DocumentAttribute(b) => b.span(),
1323 }
1324 }
1325}
1326
1327/// The five admonition labels (`NOTE`, `TIP`, …). A style naming one of these
1328/// is always recognized: it turns the block into an admonition.
1329const ADMONITION_STYLES: &[&str] = &["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"];
1330
1331/// Block style keywords this parser understands that are *not* themselves
1332/// built-in contexts.
1333///
1334/// A declared block style (the first positional attribute, e.g. `[source]`) is
1335/// recognized when it names a built-in context this parser can adopt (any
1336/// [`is_built_in_context`] style – `example`, `listing`, `sidebar`, `image`,
1337/// `table`, and so on), one of these interpreting keywords, or – via
1338/// [`ADMONITION_STYLES`] – an admonition label. Anything else is an unknown
1339/// style.
1340///
1341/// Deferring the built-in contexts to [`is_built_in_context`] keeps this check
1342/// in lock-step with
1343/// [`resolved_context`](crate::blocks::IsBlock::resolved_context), which adopts
1344/// exactly those styles as the block's context: a style that
1345/// `resolved_context` acts on is never reported as unknown. These keywords are
1346/// the remaining styles this parser interprets without their being a context –
1347/// `source` specializes `listing`, `abstract` an open block, the `stem`
1348/// flavors a `stem` block, and `comment`/`normal`/`partintro` round out
1349/// Asciidoctor's `PARAGRAPH_STYLES`.
1350const KNOWN_STYLE_KEYWORDS: &[&str] = &[
1351 "abstract",
1352 "asciimath",
1353 "comment",
1354 "latexmath",
1355 "normal",
1356 "partintro",
1357 "source",
1358];
1359
1360/// The block contexts for which an unknown declared style is diagnosed.
1361///
1362/// Asciidoctor reports an unknown style only for delimited blocks and
1363/// paragraphs; a style on a list, section, media macro, or thematic break is
1364/// interpreted differently (a list marker style, a discrete-heading style,
1365/// etc.) and is never reported this way. These are the corresponding
1366/// [`raw_context`](crate::blocks::IsBlock::raw_context) values.
1367///
1368/// [`raw_context`]: crate::blocks::IsBlock::raw_context
1369const STYLED_BLOCK_CONTEXTS: &[&str] = &[
1370 "comment",
1371 "example",
1372 "listing",
1373 "literal",
1374 "open",
1375 "paragraph",
1376 "pass",
1377 "quote",
1378 "sidebar",
1379 "stem",
1380 "table",
1381 "verse",
1382];
1383
1384/// Diagnose a block that declared a style this parser does not recognize for
1385/// its context, returning the [`WarningType`] to record or `None` when the
1386/// block has no style, the style is recognized, or the block's context is not
1387/// one for which an unknown style is reported.
1388///
1389/// The block keeps the default context implied by its syntax regardless; the
1390/// diagnostic only reports that the declared style had no effect. This mirrors
1391/// Asciidoctor, which logs `unknown style for <context> block: <style>` at
1392/// debug level and falls back to the block's context.
1393fn unknown_block_style_warning(block: &Block<'_>) -> Option<WarningType> {
1394 let style = block.declared_style()?;
1395 let context = block.raw_context();
1396 let context = context.as_ref();
1397
1398 // Only diagnose something that actually looks like a style name. A malformed
1399 // attribute list (for example a mangled `[[anchor]`, or a positional whose
1400 // value is `-foo = bar`) can leave debris in the first positional slot that
1401 // is not a plausible style; reporting it as an "unknown style" would be
1402 // noise. Asciidoctor only reaches this check with a properly-parsed style
1403 // token, so a value carrying spaces, brackets, or other punctuation is not
1404 // one this diagnostic is meant for.
1405 if !is_plausible_style_name(style) {
1406 return None;
1407 }
1408
1409 // Only delimited blocks and paragraphs report an unknown style; on any
1410 // other block a first positional attribute means something else.
1411 if !STYLED_BLOCK_CONTEXTS.contains(&context) {
1412 return None;
1413 }
1414
1415 if is_built_in_context(style)
1416 || KNOWN_STYLE_KEYWORDS.contains(&style)
1417 || ADMONITION_STYLES.contains(&style)
1418 {
1419 return None;
1420 }
1421
1422 Some(WarningType::UnknownBlockStyle(
1423 context.to_string(),
1424 style.to_string(),
1425 ))
1426}
1427
1428/// Whether `style` looks like an authored block-style name – a non-empty token
1429/// of ASCII letters, digits, `_`, or `-`. This screens out the debris a
1430/// malformed attribute list can leave in the first positional slot (values
1431/// carrying spaces, brackets, `=`, quotes, and so on), which is not a style the
1432/// unknown-style diagnostic is meant to report.
1433fn is_plausible_style_name(style: &str) -> bool {
1434 !style.is_empty()
1435 && style
1436 .chars()
1437 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442 #![allow(clippy::unwrap_used)]
1443
1444 mod unknown_block_style {
1445 use crate::{
1446 Parser,
1447 warnings::{WarningSeverity, WarningType},
1448 };
1449
1450 /// The single warning a parse produced, or `None` if there were none.
1451 fn only_warning(input: &str) -> Option<(WarningSeverity, WarningType)> {
1452 let doc = Parser::default().parse(input);
1453 let mut warnings = doc.warnings();
1454 let warning = warnings.next()?;
1455 assert!(
1456 warnings.next().is_none(),
1457 "expected at most one warning for {input:?}"
1458 );
1459
1460 Some((warning.severity, warning.warning.clone()))
1461 }
1462
1463 #[test]
1464 fn unknown_style_on_open_block_is_debug() {
1465 assert_eq!(
1466 only_warning("[foo]\n--\nbar\n--\n"),
1467 Some((
1468 WarningSeverity::Debug,
1469 WarningType::UnknownBlockStyle("open".to_string(), "foo".to_string())
1470 ))
1471 );
1472 }
1473
1474 #[test]
1475 fn unknown_style_on_paragraph_is_debug() {
1476 assert_eq!(
1477 only_warning("[foo]\nbar\n"),
1478 Some((
1479 WarningSeverity::Debug,
1480 WarningType::UnknownBlockStyle("paragraph".to_string(), "foo".to_string())
1481 ))
1482 );
1483 }
1484
1485 #[test]
1486 fn nested_unknown_style_is_reported() {
1487 assert_eq!(
1488 only_warning("====\n[bar]\n--\nx\n--\n====\n"),
1489 Some((
1490 WarningSeverity::Debug,
1491 WarningType::UnknownBlockStyle("open".to_string(), "bar".to_string())
1492 ))
1493 );
1494 }
1495
1496 #[test]
1497 fn recognized_context_style_does_not_warn() {
1498 // A built-in context masquerading over a delimited block.
1499 assert_eq!(only_warning("[example]\n--\nx\n--\n"), None);
1500 assert_eq!(only_warning("[sidebar]\n--\nx\n--\n"), None);
1501 }
1502
1503 #[test]
1504 fn recognized_keyword_style_does_not_warn() {
1505 assert_eq!(only_warning("[source]\n----\nx\n----\n"), None);
1506 assert_eq!(only_warning("[verse]\n____\nx\n____\n"), None);
1507 assert_eq!(only_warning("[abstract]\n--\nx\n--\n"), None);
1508 assert_eq!(only_warning("[asciimath]\n++++\nx\n++++\n"), None);
1509 }
1510
1511 #[test]
1512 fn any_built_in_context_style_does_not_warn() {
1513 // Every built-in context this parser can adopt as a block's context
1514 // (via `resolved_context`) is a recognized style, even one it does
1515 // not otherwise treat as a masquerade keyword (e.g. `image`,
1516 // `audio`, `video`, `table`). Reporting these would contradict the
1517 // context the parser actually resolved.
1518 assert_eq!(only_warning("[image]\nbar\n"), None);
1519 assert_eq!(only_warning("[audio]\n--\nx\n--\n"), None);
1520 assert_eq!(only_warning("[video]\nbar\n"), None);
1521 assert_eq!(only_warning("[table]\nbar\n"), None);
1522 }
1523
1524 #[test]
1525 fn admonition_style_does_not_warn() {
1526 assert_eq!(only_warning("[NOTE]\n--\nx\n--\n"), None);
1527 }
1528
1529 #[test]
1530 fn style_naming_its_own_context_does_not_warn() {
1531 // A `[table]` style on a table names the block's own context.
1532 assert_eq!(only_warning("[table]\n|===\n| x\n|===\n"), None);
1533 }
1534
1535 #[test]
1536 fn style_on_list_or_section_does_not_warn() {
1537 // A first positional on a list or a section heading is not a block
1538 // style (a list marker style, a discrete-heading flag, and so on).
1539 assert_eq!(only_warning("[square]\n* a\n* b\n"), None);
1540 assert_eq!(only_warning("[discrete]\n== Heading\n"), None);
1541 }
1542
1543 #[test]
1544 fn malformed_attrlist_debris_does_not_warn() {
1545 // A mangled anchor (`[[notice]`) leaves `[notice` in the first
1546 // positional slot; it is not a plausible style name.
1547 assert_eq!(only_warning("[[notice]\nThis is a paragraph.\n"), None);
1548 }
1549 }
1550
1551 mod is_plausible_style_name {
1552 use crate::blocks::block::is_plausible_style_name;
1553
1554 #[test]
1555 fn accepts_style_tokens() {
1556 assert!(is_plausible_style_name("foo"));
1557 assert!(is_plausible_style_name("NOTE"));
1558 assert!(is_plausible_style_name("foo-bar"));
1559 assert!(is_plausible_style_name("foo_bar"));
1560 assert!(is_plausible_style_name("style2"));
1561 }
1562
1563 #[test]
1564 fn rejects_debris_and_empty() {
1565 assert!(!is_plausible_style_name(""));
1566 assert!(!is_plausible_style_name("[notice"));
1567 assert!(!is_plausible_style_name("-foo = bar"));
1568 assert!(!is_plausible_style_name("a,b"));
1569 assert!(!is_plausible_style_name("has space"));
1570 }
1571 }
1572
1573 mod resolved_style {
1574 use crate::{
1575 Parser,
1576 blocks::{FindBlocks, IsBlock},
1577 };
1578
1579 #[test]
1580 fn forwards_to_every_block_variant() {
1581 // `Block::resolved_style` forwards to each variant. Exercise every
1582 // arm with a document that contains one of each block kind, and
1583 // confirm the forwarded value: none of these blocks acquires an
1584 // implicit style, so the resolved style equals the declared style
1585 // for every one. (The one case where the two differ – a bibliography
1586 // list that inherits its style from its section – is covered by the
1587 // list block's own tests.)
1588 let doc = Parser::default().parse(
1589 "= Doc Title\n\nPreamble para.\n\nimage::pic.png[]\n\n'''\n\ntoc::[]\n\n== Section One\n\n:body-attr: x\n\nA paragraph.\n\n* list item\n\n[quote]\n____\nA quote.\n____\n\n[NOTE]\n====\nAn admonition.\n====\n\n----\nlisting\n----\n\n|===\n| cell\n|===\n\n--\nopen block\n--\n",
1590 );
1591
1592 let mut count = 0;
1593 for block in doc.descendant_blocks() {
1594 assert_eq!(block.resolved_style(), block.declared_style());
1595 count += 1;
1596 }
1597
1598 // Guard against the document silently parsing into fewer blocks than
1599 // the variants it is meant to cover.
1600 assert!(
1601 count >= 14,
1602 "expected the sample document to yield every block variant, saw {count} blocks"
1603 );
1604 }
1605 }
1606}