Skip to main content

asciidoc_parser/blocks/
block.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    attributes::Attrlist,
6    blocks::{
7        Break, CompoundDelimitedBlock, ContentModel, IsBlock, ListBlock, ListItem, ListItemMarker,
8        MediaBlock, Preamble, RawDelimitedBlock, SectionBlock, SimpleBlock, TableBlock,
9        metadata::BlockMetadata,
10    },
11    content::{Content, SubstitutionGroup},
12    document::{Attribute, RefType},
13    parser::{InlineSubstitutionRenderer, ReferenceResolver, ReferenceWarning},
14    span::MatchedItem,
15    strings::CowStr,
16    warnings::{MatchAndWarnings, Warning, WarningType},
17};
18
19/// **Block elements** form the main structure of an AsciiDoc document, starting
20/// with the document itself.
21///
22/// A block element (aka **block**) is a discrete, line-oriented chunk of
23/// content in an AsciiDoc document. Once parsed, that chunk of content becomes
24/// a block element in the parsed document model. Certain blocks may contain
25/// other blocks, so we say that blocks can be nested. The converter visits each
26/// block in turn, in document order, converting it to a corresponding chunk of
27/// output.
28///
29/// This enum represents all of the block types that are understood directly by
30/// this parser and also implements the [`IsBlock`] trait.
31#[derive(Clone, Eq, PartialEq)]
32#[allow(clippy::large_enum_variant)] // TEMPORARY: review later
33#[non_exhaustive]
34pub enum Block<'src> {
35    /// A block that’s treated as contiguous lines of paragraph text (and
36    /// subject to normal substitutions) (e.g., a paragraph block).
37    Simple(SimpleBlock<'src>),
38
39    /// A media block is used to represent an image, video, or audio block
40    /// macro.
41    Media(MediaBlock<'src>),
42
43    /// A section helps to partition the document into a content hierarchy.
44    /// May also be a part, chapter, or special section.
45    Section(SectionBlock<'src>),
46
47    /// A list contains a sequence of items prefixed with symbol, such as a disc
48    /// (aka bullet). Each individual item in the list is represented by a
49    /// [`ListItem`].
50    List(ListBlock<'src>),
51
52    /// A list item is a special kind of block that is a member of a
53    /// [`ListBlock`] and contains one or more blocks attached to it.
54    ListItem(ListItem<'src>),
55
56    /// A delimited block that contains verbatim, raw, or comment text. The
57    /// content between the matching delimiters is not parsed for block
58    /// syntax.
59    RawDelimited(RawDelimitedBlock<'src>),
60
61    /// A delimited block that can contain other blocks.
62    CompoundDelimited(CompoundDelimitedBlock<'src>),
63
64    /// A table block arranges content into a grid of rows and columns.
65    Table(TableBlock<'src>),
66
67    /// Content between the end of the document header and the first section
68    /// title in the document body is called the preamble.
69    Preamble(Preamble<'src>),
70
71    /// A thematic or page break.
72    Break(Break<'src>),
73
74    /// When an attribute is defined in the document body using an attribute
75    /// entry, that’s simply referred to as a document attribute.
76    DocumentAttribute(Attribute<'src>),
77}
78
79impl<'src> std::fmt::Debug for Block<'src> {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Block::Simple(block) => f.debug_tuple("Block::Simple").field(block).finish(),
83            Block::Media(block) => f.debug_tuple("Block::Media").field(block).finish(),
84            Block::Section(block) => f.debug_tuple("Block::Section").field(block).finish(),
85            Block::List(block) => f.debug_tuple("Block::List").field(block).finish(),
86            Block::ListItem(block) => f.debug_tuple("Block::ListItem").field(block).finish(),
87
88            Block::RawDelimited(block) => {
89                f.debug_tuple("Block::RawDelimited").field(block).finish()
90            }
91
92            Block::CompoundDelimited(block) => f
93                .debug_tuple("Block::CompoundDelimited")
94                .field(block)
95                .finish(),
96
97            Block::Table(block) => f.debug_tuple("Block::Table").field(block).finish(),
98            Block::Preamble(block) => f.debug_tuple("Block::Preamble").field(block).finish(),
99            Block::Break(break_) => f.debug_tuple("Block::Break").field(break_).finish(),
100
101            Block::DocumentAttribute(block) => f
102                .debug_tuple("Block::DocumentAttribute")
103                .field(block)
104                .finish(),
105        }
106    }
107}
108
109impl<'src> Block<'src> {
110    /// Parse a block of any type and return a `Block` that describes it.
111    ///
112    /// Consumes any blank lines before and after the block.
113    pub(crate) fn parse(
114        source: Span<'src>,
115        parser: &mut Parser,
116    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
117        Self::parse_internal(source, parser, None, false)
118    }
119
120    /// Parse a block of any type and return a `Block` that describes it.
121    ///
122    /// Will terminate early when parsing certain block types within a list
123    /// context.
124    ///
125    /// Consumes any blank lines before and after the block.
126    ///
127    /// If `is_continuation` is true, this content was attached via a `+`
128    /// continuation marker and literal blocks should preserve their
129    /// indentation.
130    pub(crate) fn parse_for_list_item(
131        source: Span<'src>,
132        parser: &mut Parser,
133        parent_list_markers: &[ListItemMarker<'src>],
134        is_continuation: bool,
135    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
136        Self::parse_internal(source, parser, Some(parent_list_markers), is_continuation)
137    }
138
139    /// Shared parser for [`Block::parse`] and [`Block::parse_for_list_item`].
140    fn parse_internal(
141        source: Span<'src>,
142        parser: &mut Parser,
143        parent_list_markers: Option<&[ListItemMarker<'src>]>,
144        is_continuation: bool,
145    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
146        // Optimization: If the first line doesn't match any of the early indications
147        // for delimited blocks, titles, or attrlists, we can skip directly to treating
148        // this as a simple block. That saves quite a bit of parsing time.
149        let first_line = source.take_line().item.discard_whitespace();
150
151        // If it does contain any of those markers, we fall through to the more costly
152        // tests below which can more accurately classify the upcoming block.
153        if let Some(first_char) = first_line.chars().next()
154            && !matches!(
155                first_char,
156                '.' | '#' | '=' | '/' | '-' | '+' | '*' | '_' | '[' | ':' | '\'' | '<' | '•'
157            )
158            && !first_line.contains("::")
159            && !first_line.contains(";;")
160            && !TableBlock::is_table_delimiter(&first_line)
161            && !ListItemMarker::starts_with_marker(first_line)
162            && parent_list_markers.is_none()
163            && let Some(MatchedItem {
164                item: simple_block,
165                after,
166            }) = SimpleBlock::parse_fast(source, parser)
167        {
168            let mut warnings = vec![];
169            let block = Self::Simple(simple_block);
170
171            Self::register_block_id(
172                block.id(),
173                block.title(),
174                block.span(),
175                parser,
176                &mut warnings,
177            );
178
179            return MatchAndWarnings {
180                item: Some(MatchedItem { item: block, after }),
181                warnings,
182            };
183        }
184
185        // Look for document attributes first since these don't support block metadata.
186        if first_line.starts_with(':')
187            && (first_line.ends_with(':') || first_line.contains(": "))
188            && let Some(attr) = Attribute::parse(source, parser)
189        {
190            let mut warnings: Vec<Warning<'src>> = vec![];
191            parser.set_attribute_from_body(&attr.item, &mut warnings);
192
193            return MatchAndWarnings {
194                item: Some(MatchedItem {
195                    item: Self::DocumentAttribute(attr.item),
196                    after: attr.after,
197                }),
198                warnings,
199            };
200        }
201
202        // Optimization not possible; start by looking for block metadata (title,
203        // attrlist, etc.).
204        let MatchAndWarnings {
205            item: mut metadata,
206            mut warnings,
207        } = BlockMetadata::parse(source, parser);
208
209        let is_literal =
210            metadata.attrlist.as_ref().and_then(|a| a.block_style()) == Some("literal");
211
212        if !is_literal {
213            if let Some(mut rdb_maw) = RawDelimitedBlock::parse(&metadata, parser)
214                && let Some(rdb) = rdb_maw.item
215            {
216                if !rdb_maw.warnings.is_empty() {
217                    warnings.append(&mut rdb_maw.warnings);
218                }
219
220                let block = Self::RawDelimited(rdb.item);
221
222                Self::register_block_id(
223                    block.id(),
224                    block.title(),
225                    block.span(),
226                    parser,
227                    &mut warnings,
228                );
229
230                return MatchAndWarnings {
231                    item: Some(MatchedItem {
232                        item: block,
233                        after: rdb.after,
234                    }),
235                    warnings,
236                };
237            }
238
239            if let Some(mut cdb_maw) = CompoundDelimitedBlock::parse(&metadata, parser)
240                && let Some(cdb) = cdb_maw.item
241            {
242                if !cdb_maw.warnings.is_empty() {
243                    warnings.append(&mut cdb_maw.warnings);
244                }
245
246                let block = Self::CompoundDelimited(cdb.item);
247
248                Self::register_block_id(
249                    block.id(),
250                    block.title(),
251                    block.span(),
252                    parser,
253                    &mut warnings,
254                );
255
256                return MatchAndWarnings {
257                    item: Some(MatchedItem {
258                        item: block,
259                        after: cdb.after,
260                    }),
261                    warnings,
262                };
263            }
264
265            if let Some(mut table_maw) = TableBlock::parse(&metadata, parser)
266                && let Some(table) = table_maw.item
267            {
268                if !table_maw.warnings.is_empty() {
269                    warnings.append(&mut table_maw.warnings);
270                }
271
272                let block = Self::Table(table.item);
273
274                Self::register_block_id(
275                    block.id(),
276                    block.title(),
277                    block.span(),
278                    parser,
279                    &mut warnings,
280                );
281
282                return MatchAndWarnings {
283                    item: Some(MatchedItem {
284                        item: block,
285                        after: table.after,
286                    }),
287                    warnings,
288                };
289            }
290
291            // Try to discern the block type by scanning the first line.
292            let line = metadata.block_start.take_normalized_line();
293
294            if line.item.starts_with("image::")
295                || line.item.starts_with("video::")
296                || line.item.starts_with("video::")
297            {
298                let mut media_block_maw = MediaBlock::parse(&metadata, parser);
299
300                if let Some(media_block) = media_block_maw.item {
301                    // Only propagate warnings from media block parsing if we think this
302                    // *is* a media block. Otherwise, there would likely be too many false
303                    // positives.
304                    if !media_block_maw.warnings.is_empty() {
305                        warnings.append(&mut media_block_maw.warnings);
306                    }
307
308                    let block = Self::Media(media_block.item);
309
310                    Self::register_block_id(
311                        block.id(),
312                        block.title(),
313                        block.span(),
314                        parser,
315                        &mut warnings,
316                    );
317
318                    return MatchAndWarnings {
319                        item: Some(MatchedItem {
320                            item: block,
321                            after: media_block.after,
322                        }),
323                        warnings,
324                    };
325                }
326
327                // This might be some other kind of block, so we don't
328                // automatically error out on a parse failure.
329            }
330
331            if (line.item.starts_with('=') || line.item.starts_with('#'))
332                && let Some(mi_section_block) =
333                    SectionBlock::parse(&metadata, parser, &mut warnings)
334            {
335                // A line starting with `=` or `#` might be some other kind of block, so we
336                // continue quietly if `SectionBlock` parser rejects this block.
337
338                return MatchAndWarnings {
339                    item: Some(MatchedItem {
340                        item: Self::Section(mi_section_block.item),
341                        after: mi_section_block.after,
342                    }),
343                    warnings,
344                };
345            }
346
347            if (line.item.starts_with('\'')
348                || line.item.starts_with('-')
349                || line.item.starts_with('*')
350                || line.item.starts_with('<'))
351                && let Some(mi_break) = Break::parse(&metadata, parser)
352            {
353                // Continue quietly if `Break` parser rejects this block.
354
355                return MatchAndWarnings {
356                    item: Some(MatchedItem {
357                        item: Self::Break(mi_break.item),
358                        after: mi_break.after,
359                    }),
360                    warnings,
361                };
362            }
363
364            // Only try to parse as a new list if we're NOT inside a list item context.
365            // If we are inside a list context, lists can only be created when the first
366            // line is a list item marker (handled above).
367            if parent_list_markers.is_none()
368                && let Some(mi_list) = ListBlock::parse(&metadata, parser, &mut warnings)
369            {
370                return MatchAndWarnings {
371                    item: Some(MatchedItem {
372                        item: Self::List(mi_list.item),
373                        after: mi_list.after,
374                    }),
375                    warnings,
376                };
377            }
378
379            // First, let's look for a fun edge case. Perhaps the text contains block
380            // metadata but no block immediately following. If we're not careful, we could
381            // spin in a loop (for example, `parse_blocks_until`) thinking there will be
382            // another block, but there isn't.
383
384            // The following check disables that spin loop.
385            let simple_block_mi = if let Some(plm) = parent_list_markers {
386                SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
387            } else {
388                SimpleBlock::parse(&metadata, parser)
389            };
390
391            if simple_block_mi.is_none() && !metadata.is_empty() {
392                // We have a metadata with no block. Treat it as a simple block but issue a
393                // warning.
394
395                warnings.push(Warning {
396                    source: metadata.source,
397                    warning: WarningType::MissingBlockAfterTitleOrAttributeList,
398                });
399
400                // Remove the metadata content so that SimpleBlock will read the title/attrlist
401                // line(s) as regular content.
402                metadata.title_source = None;
403                metadata.title = None;
404                metadata.anchor = None;
405                metadata.attrlist = None;
406                metadata.block_start = metadata.source;
407            }
408        }
409
410        // If no other block kind matches, we can always use SimpleBlock.
411        let simple_block_mi = if let Some(plm) = parent_list_markers {
412            SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
413        } else {
414            SimpleBlock::parse(&metadata, parser)
415        };
416
417        let mut result = MatchAndWarnings {
418            item: simple_block_mi.map(|mi| MatchedItem {
419                item: Self::Simple(mi.item),
420                after: mi.after,
421            }),
422            warnings,
423        };
424
425        if let Some(ref matched_item) = result.item {
426            Self::register_block_id(
427                matched_item.item.id(),
428                matched_item.item.title(),
429                matched_item.item.span(),
430                parser,
431                &mut result.warnings,
432            );
433        }
434
435        result
436    }
437
438    /// Register a block's ID with the catalog if the block has an ID.
439    ///
440    /// This should be called for all block types except `SectionBlock`,
441    /// which handles its own catalog registration.
442    fn register_block_id(
443        id: Option<&str>,
444        title: Option<&str>,
445        span: Span<'src>,
446        parser: &mut Parser,
447        warnings: &mut Vec<Warning<'src>>,
448    ) {
449        if let Some(id) = id
450            && let Err(_duplicate_error) = parser.register_ref(
451                id,
452                title, // Use block title as reftext if available
453                RefType::Anchor,
454            )
455        {
456            // If registration fails due to duplicate ID, issue a warning.
457            warnings.push(Warning {
458                source: span,
459                warning: WarningType::DuplicateId(id.to_string()),
460            });
461        }
462    }
463
464    /// Returns a reference to the inner [`ListItem`] if this is a
465    /// `Block::ListItem`, or `None` otherwise.
466    pub(crate) fn as_list_item(&self) -> Option<&ListItem<'src>> {
467        match self {
468            Self::ListItem(li) => Some(li),
469            _ => None,
470        }
471    }
472
473    /// Resolve any deferred cross-references in this block and its descendants,
474    /// using `resolver` to map targets to destinations and `renderer` to render
475    /// the resulting links. Unresolved targets are reported in `warnings`.
476    ///
477    /// This drives the recursion uniformly via the [`IsBlock::content_mut`] and
478    /// [`IsBlock::nested_blocks_mut`] accessors, so it needs no per-block-type
479    /// special casing.
480    pub(crate) fn resolve_references(
481        &mut self,
482        resolver: &dyn ReferenceResolver,
483        renderer: &dyn InlineSubstitutionRenderer,
484        warnings: &mut Vec<ReferenceWarning>,
485    ) {
486        if let Some(content) = self.content_mut() {
487            content.resolve_references(resolver, renderer, warnings);
488        }
489
490        // Tables hold their resolvable content in cells rather than in a single
491        // `content_mut()` value, so they are resolved explicitly here.
492        if let Self::Table(table) = self {
493            table.resolve_references(resolver, renderer, warnings);
494        }
495
496        for child in self.nested_blocks_mut() {
497            child.resolve_references(resolver, renderer, warnings);
498        }
499    }
500}
501
502impl<'src> IsBlock<'src> for Block<'src> {
503    fn content_model(&self) -> ContentModel {
504        match self {
505            Self::Simple(_) => ContentModel::Simple,
506            Self::Media(b) => b.content_model(),
507            Self::Section(_) => ContentModel::Compound,
508            Self::List(b) => b.content_model(),
509            Self::ListItem(b) => b.content_model(),
510            Self::RawDelimited(b) => b.content_model(),
511            Self::CompoundDelimited(b) => b.content_model(),
512            Self::Table(b) => b.content_model(),
513            Self::Preamble(b) => b.content_model(),
514            Self::Break(b) => b.content_model(),
515            Self::DocumentAttribute(b) => b.content_model(),
516        }
517    }
518
519    fn rendered_content(&'src self) -> Option<&'src str> {
520        match self {
521            Self::Simple(b) => b.rendered_content(),
522            Self::Media(b) => b.rendered_content(),
523            Self::Section(b) => b.rendered_content(),
524            Self::List(b) => b.rendered_content(),
525            Self::ListItem(b) => b.rendered_content(),
526            Self::RawDelimited(b) => b.rendered_content(),
527            Self::CompoundDelimited(b) => b.rendered_content(),
528            Self::Table(b) => b.rendered_content(),
529            Self::Preamble(b) => b.rendered_content(),
530            Self::Break(b) => b.rendered_content(),
531            Self::DocumentAttribute(b) => b.rendered_content(),
532        }
533    }
534
535    fn raw_context(&self) -> CowStr<'src> {
536        match self {
537            Self::Simple(b) => b.raw_context(),
538            Self::Media(b) => b.raw_context(),
539            Self::Section(b) => b.raw_context(),
540            Self::List(b) => b.raw_context(),
541            Self::ListItem(b) => b.raw_context(),
542            Self::RawDelimited(b) => b.raw_context(),
543            Self::CompoundDelimited(b) => b.raw_context(),
544            Self::Table(b) => b.raw_context(),
545            Self::Preamble(b) => b.raw_context(),
546            Self::Break(b) => b.raw_context(),
547            Self::DocumentAttribute(b) => b.raw_context(),
548        }
549    }
550
551    fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
552        match self {
553            Self::Simple(b) => b.nested_blocks(),
554            Self::Media(b) => b.nested_blocks(),
555            Self::Section(b) => b.nested_blocks(),
556            Self::List(b) => b.nested_blocks(),
557            Self::ListItem(b) => b.nested_blocks(),
558            Self::RawDelimited(b) => b.nested_blocks(),
559            Self::CompoundDelimited(b) => b.nested_blocks(),
560            Self::Table(b) => b.nested_blocks(),
561            Self::Preamble(b) => b.nested_blocks(),
562            Self::Break(b) => b.nested_blocks(),
563            Self::DocumentAttribute(b) => b.nested_blocks(),
564        }
565    }
566
567    fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
568        match self {
569            Self::Simple(b) => b.nested_blocks_mut(),
570            Self::Media(b) => b.nested_blocks_mut(),
571            Self::Section(b) => b.nested_blocks_mut(),
572            Self::List(b) => b.nested_blocks_mut(),
573            Self::ListItem(b) => b.nested_blocks_mut(),
574            Self::RawDelimited(b) => b.nested_blocks_mut(),
575            Self::CompoundDelimited(b) => b.nested_blocks_mut(),
576            Self::Table(b) => b.nested_blocks_mut(),
577            Self::Preamble(b) => b.nested_blocks_mut(),
578            Self::Break(b) => b.nested_blocks_mut(),
579            Self::DocumentAttribute(b) => b.nested_blocks_mut(),
580        }
581    }
582
583    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
584        match self {
585            Self::Simple(b) => b.content_mut(),
586            Self::Media(b) => b.content_mut(),
587            Self::Section(b) => b.content_mut(),
588            Self::List(b) => b.content_mut(),
589            Self::ListItem(b) => b.content_mut(),
590            Self::RawDelimited(b) => b.content_mut(),
591            Self::CompoundDelimited(b) => b.content_mut(),
592            Self::Table(b) => b.content_mut(),
593            Self::Preamble(b) => b.content_mut(),
594            Self::Break(b) => b.content_mut(),
595            Self::DocumentAttribute(b) => b.content_mut(),
596        }
597    }
598
599    fn title_source(&'src self) -> Option<Span<'src>> {
600        match self {
601            Self::Simple(b) => b.title_source(),
602            Self::Media(b) => b.title_source(),
603            Self::Section(b) => b.title_source(),
604            Self::List(b) => b.title_source(),
605            Self::ListItem(b) => b.title_source(),
606            Self::RawDelimited(b) => b.title_source(),
607            Self::CompoundDelimited(b) => b.title_source(),
608            Self::Table(b) => b.title_source(),
609            Self::Preamble(b) => b.title_source(),
610            Self::Break(b) => b.title_source(),
611            Self::DocumentAttribute(b) => b.title_source(),
612        }
613    }
614
615    fn title(&self) -> Option<&str> {
616        match self {
617            Self::Simple(b) => b.title(),
618            Self::Media(b) => b.title(),
619            Self::Section(b) => b.title(),
620            Self::List(b) => b.title(),
621            Self::ListItem(b) => b.title(),
622            Self::RawDelimited(b) => b.title(),
623            Self::CompoundDelimited(b) => b.title(),
624            Self::Table(b) => b.title(),
625            Self::Preamble(b) => b.title(),
626            Self::Break(b) => b.title(),
627            Self::DocumentAttribute(b) => b.title(),
628        }
629    }
630
631    fn anchor(&'src self) -> Option<Span<'src>> {
632        match self {
633            Self::Simple(b) => b.anchor(),
634            Self::Media(b) => b.anchor(),
635            Self::Section(b) => b.anchor(),
636            Self::List(b) => b.anchor(),
637            Self::ListItem(b) => b.anchor(),
638            Self::RawDelimited(b) => b.anchor(),
639            Self::CompoundDelimited(b) => b.anchor(),
640            Self::Table(b) => b.anchor(),
641            Self::Preamble(b) => b.anchor(),
642            Self::Break(b) => b.anchor(),
643            Self::DocumentAttribute(b) => b.anchor(),
644        }
645    }
646
647    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
648        match self {
649            Self::Simple(b) => b.anchor_reftext(),
650            Self::Media(b) => b.anchor_reftext(),
651            Self::Section(b) => b.anchor_reftext(),
652            Self::List(b) => b.anchor_reftext(),
653            Self::ListItem(b) => b.anchor_reftext(),
654            Self::RawDelimited(b) => b.anchor_reftext(),
655            Self::CompoundDelimited(b) => b.anchor_reftext(),
656            Self::Table(b) => b.anchor_reftext(),
657            Self::Preamble(b) => b.anchor_reftext(),
658            Self::Break(b) => b.anchor_reftext(),
659            Self::DocumentAttribute(b) => b.anchor_reftext(),
660        }
661    }
662
663    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
664        match self {
665            Self::Simple(b) => b.attrlist(),
666            Self::Media(b) => b.attrlist(),
667            Self::Section(b) => b.attrlist(),
668            Self::List(b) => b.attrlist(),
669            Self::ListItem(b) => b.attrlist(),
670            Self::RawDelimited(b) => b.attrlist(),
671            Self::CompoundDelimited(b) => b.attrlist(),
672            Self::Table(b) => b.attrlist(),
673            Self::Preamble(b) => b.attrlist(),
674            Self::Break(b) => b.attrlist(),
675            Self::DocumentAttribute(b) => b.attrlist(),
676        }
677    }
678
679    fn substitution_group(&self) -> SubstitutionGroup {
680        match self {
681            Self::Simple(b) => b.substitution_group(),
682            Self::Media(b) => b.substitution_group(),
683            Self::Section(b) => b.substitution_group(),
684            Self::List(b) => b.substitution_group(),
685            Self::ListItem(b) => b.substitution_group(),
686            Self::RawDelimited(b) => b.substitution_group(),
687            Self::CompoundDelimited(b) => b.substitution_group(),
688            Self::Table(b) => b.substitution_group(),
689            Self::Preamble(b) => b.substitution_group(),
690            Self::Break(b) => b.substitution_group(),
691            Self::DocumentAttribute(b) => b.substitution_group(),
692        }
693    }
694}
695
696impl<'src> HasSpan<'src> for Block<'src> {
697    fn span(&self) -> Span<'src> {
698        match self {
699            Self::Simple(b) => b.span(),
700            Self::Media(b) => b.span(),
701            Self::Section(b) => b.span(),
702            Self::List(b) => b.span(),
703            Self::ListItem(b) => b.span(),
704            Self::RawDelimited(b) => b.span(),
705            Self::CompoundDelimited(b) => b.span(),
706            Self::Table(b) => b.span(),
707            Self::Preamble(b) => b.span(),
708            Self::Break(b) => b.span(),
709            Self::DocumentAttribute(b) => b.span(),
710        }
711    }
712}