asciidoc_parser/blocks/
raw_delimited.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5    content::{Content, SubstitutionGroup},
6    span::MatchedItem,
7    strings::CowStr,
8    warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11/// A delimited block that contains verbatim, raw, or comment text. The content
12/// between the matching delimiters is not parsed for block syntax.
13///
14/// The following delimiters are recognized as raw delimited blocks:
15///
16/// | Delimiter | Content type |
17/// |-----------|--------------|
18/// | `////`    | Comment      |
19/// | `----`    | Listing      |
20/// | `....`    | Literal      |
21/// | `++++`    | Passthrough  |
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct RawDelimitedBlock<'src> {
24    content: Content<'src>,
25    content_model: ContentModel,
26    context: CowStr<'src>,
27    source: Span<'src>,
28    title_source: Option<Span<'src>>,
29    title: Option<String>,
30    anchor: Option<Span<'src>>,
31    attrlist: Option<Attrlist<'src>>,
32    substitution_group: SubstitutionGroup,
33}
34
35impl<'src> RawDelimitedBlock<'src> {
36    pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
37        let data = line.data();
38
39        // TO DO (https://github.com/scouten/asciidoc-parser/issues/145):
40        // Seek spec clarity: Do the characters after the fourth char
41        // have to match the first four?
42
43        if data.len() >= 4 {
44            if data.starts_with("////") {
45                data.split_at(4).1.chars().all(|c| c == '/')
46            } else if data.starts_with("----") {
47                data.split_at(4).1.chars().all(|c| c == '-')
48            } else 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 {
53                false
54            }
55        } else {
56            false
57        }
58    }
59
60    pub(crate) fn parse(
61        metadata: &BlockMetadata<'src>,
62        parser: &mut Parser,
63    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
64        let delimiter = metadata.block_start.take_normalized_line();
65
66        if delimiter.item.len() < 4 {
67            return None;
68        }
69
70        let (content_model, context, mut substitution_group) =
71            match delimiter.item.data().split_at(4).0 {
72                "////" => (ContentModel::Raw, "comment", SubstitutionGroup::None),
73                "----" => (
74                    ContentModel::Verbatim,
75                    "listing",
76                    SubstitutionGroup::Verbatim,
77                ),
78                "...." => (
79                    ContentModel::Verbatim,
80                    "literal",
81                    SubstitutionGroup::Verbatim,
82                ),
83                "++++" => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
84                _ => return None,
85            };
86
87        if !Self::is_valid_delimiter(&delimiter.item) {
88            return None;
89        }
90
91        let content_start = delimiter.after;
92        let mut next = content_start;
93
94        while !next.is_empty() {
95            let line = next.take_normalized_line();
96            if line.item.data() == delimiter.item.data() {
97                let content = content_start.trim_remainder(next).trim_trailing_line_end();
98
99                let mut content: Content<'src> = content.into();
100
101                substitution_group =
102                    substitution_group.override_via_attrlist(metadata.attrlist.as_ref());
103
104                substitution_group.apply(&mut content, parser, metadata.attrlist.as_ref());
105
106                return Some(MatchAndWarnings {
107                    item: Some(MatchedItem {
108                        item: Self {
109                            content,
110                            content_model,
111                            context: context.into(),
112                            source: metadata
113                                .source
114                                .trim_remainder(line.after)
115                                .trim_trailing_line_end(),
116                            title_source: metadata.title_source,
117                            title: metadata.title.clone(),
118                            anchor: metadata.anchor,
119                            attrlist: metadata.attrlist.clone(),
120                            substitution_group,
121                        },
122                        after: line.after,
123                    }),
124                    warnings: vec![],
125                });
126            }
127
128            next = line.after;
129        }
130
131        let content = content_start.trim_remainder(next).trim_trailing_line_end();
132
133        Some(MatchAndWarnings {
134            item: Some(MatchedItem {
135                item: Self {
136                    content: content.into(),
137                    content_model,
138                    context: context.into(),
139                    source: metadata
140                        .source
141                        .trim_remainder(next)
142                        .trim_trailing_line_end(),
143                    title_source: metadata.title_source,
144                    title: metadata.title.clone(),
145                    anchor: metadata.anchor,
146                    attrlist: metadata.attrlist.clone(),
147                    substitution_group,
148                },
149                after: next,
150            }),
151            warnings: vec![Warning {
152                source: delimiter.item,
153                warning: WarningType::UnterminatedDelimitedBlock,
154            }],
155        })
156    }
157
158    /// Return the interpreted content of this block.
159    pub fn content(&self) -> &Content<'src> {
160        &self.content
161    }
162}
163
164impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
165    fn content_model(&self) -> ContentModel {
166        self.content_model
167    }
168
169    fn raw_context(&self) -> CowStr<'src> {
170        self.context.clone()
171    }
172
173    fn title_source(&'src self) -> Option<Span<'src>> {
174        self.title_source
175    }
176
177    fn title(&self) -> Option<&str> {
178        self.title.as_deref()
179    }
180
181    fn anchor(&'src self) -> Option<Span<'src>> {
182        self.anchor
183    }
184
185    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
186        self.attrlist.as_ref()
187    }
188
189    fn substitution_group(&'src self) -> SubstitutionGroup {
190        self.substitution_group.clone()
191    }
192}
193
194impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
195    fn span(&self) -> Span<'src> {
196        self.source
197    }
198}