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