asciidoc_parser/blocks/
compound_delimited.rs

1use std::slice::Iter;
2
3use crate::{
4    HasSpan, Parser, Span,
5    attributes::Attrlist,
6    blocks::{
7        Block, ContentModel, IsBlock, metadata::BlockMetadata, parse_utils::parse_blocks_until,
8    },
9    span::MatchedItem,
10    strings::CowStr,
11    warnings::{MatchAndWarnings, Warning, WarningType},
12};
13
14/// A delimited block that can contain other blocks.
15///
16/// The following delimiters are recognized as compound delimited blocks:
17///
18/// | Delimiter | Content type |
19/// |-----------|--------------|
20/// | `====`    | Example      |
21/// | `--`      | Open         |
22/// | `****`    | Sidebar      |
23/// | `____`    | Quote        |
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct CompoundDelimitedBlock<'src> {
26    blocks: Vec<Block<'src>>,
27    context: CowStr<'src>,
28    source: Span<'src>,
29    title_source: Option<Span<'src>>,
30    title: Option<String>,
31    anchor: Option<Span<'src>>,
32    attrlist: Option<Attrlist<'src>>,
33}
34
35impl<'src> CompoundDelimitedBlock<'src> {
36    pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
37        let data = line.data();
38
39        if data == "--" {
40            return true;
41        }
42
43        // TO DO (https://github.com/scouten/asciidoc-parser/issues/145):
44        // Seek spec clarity: Do the characters after the fourth char
45        // have to match the first four?
46
47        if data.len() >= 4 {
48            if data.starts_with("====") {
49                data.split_at(4).1.chars().all(|c| c == '=')
50            } else if data.starts_with("****") {
51                data.split_at(4).1.chars().all(|c| c == '*')
52            } else if data.starts_with("____") {
53                data.split_at(4).1.chars().all(|c| c == '_')
54            } else {
55                false
56            }
57        } else {
58            false
59        }
60    }
61
62    pub(crate) fn parse(
63        metadata: &BlockMetadata<'src>,
64        parser: &mut Parser,
65    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
66        let delimiter = metadata.block_start.take_normalized_line();
67        let maybe_delimiter_text = delimiter.item.data();
68
69        // TO DO (https://github.com/scouten/asciidoc-parser/issues/146):
70        // Seek spec clarity on whether three hyphens can be used to
71        // delimit an open block. Assuming yes for now.
72        let context = match maybe_delimiter_text
73            .split_at(maybe_delimiter_text.len().min(4))
74            .0
75        {
76            "====" => "example",
77            "--" => "open",
78            "****" => "sidebar",
79            "____" => "quote",
80            _ => return None,
81        };
82
83        if !Self::is_valid_delimiter(&delimiter.item) {
84            return None;
85        }
86
87        let mut next = delimiter.after;
88        let (closing_delimiter, after) = loop {
89            if next.is_empty() {
90                break (next, next);
91            }
92
93            let line = next.take_normalized_line();
94            if line.item.data() == delimiter.item.data() {
95                break (line.item, line.after);
96            }
97            next = line.after;
98        };
99
100        let inside_delimiters = delimiter.after.trim_remainder(closing_delimiter);
101
102        let maw_blocks = parse_blocks_until(inside_delimiters, |_| false, parser);
103
104        let blocks = maw_blocks.item;
105        let source = metadata
106            .source
107            .trim_remainder(closing_delimiter.discard_all());
108
109        Some(MatchAndWarnings {
110            item: Some(MatchedItem {
111                item: Self {
112                    blocks: blocks.item,
113                    context: context.into(),
114                    source: source.trim_trailing_whitespace(),
115                    title_source: metadata.title_source,
116                    title: metadata.title.clone(),
117                    anchor: metadata.anchor,
118                    attrlist: metadata.attrlist.clone(),
119                },
120                after,
121            }),
122            warnings: if closing_delimiter.is_empty() {
123                let mut warnings = maw_blocks.warnings;
124                warnings.insert(
125                    0,
126                    Warning {
127                        source: delimiter.item,
128                        warning: WarningType::UnterminatedDelimitedBlock,
129                    },
130                );
131                warnings
132            } else {
133                maw_blocks.warnings
134            },
135        })
136    }
137}
138
139impl<'src> IsBlock<'src> for CompoundDelimitedBlock<'src> {
140    fn content_model(&self) -> ContentModel {
141        ContentModel::Compound
142    }
143
144    fn raw_context(&self) -> CowStr<'src> {
145        self.context.clone()
146    }
147
148    fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
149        self.blocks.iter()
150    }
151
152    fn title_source(&'src self) -> Option<Span<'src>> {
153        self.title_source
154    }
155
156    fn title(&self) -> Option<&str> {
157        self.title.as_deref()
158    }
159
160    fn anchor(&'src self) -> Option<Span<'src>> {
161        self.anchor
162    }
163
164    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
165        self.attrlist.as_ref()
166    }
167}
168
169impl<'src> HasSpan<'src> for CompoundDelimitedBlock<'src> {
170    fn span(&self) -> Span<'src> {
171        self.source
172    }
173}