Skip to main content

asciidoc_parser/blocks/
simple.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{
5        CompoundDelimitedBlock, ContentModel, IsBlock, RawDelimitedBlock, metadata::BlockMetadata,
6    },
7    content::{Content, SubstitutionGroup},
8    span::MatchedItem,
9    strings::CowStr,
10};
11
12/// The style of a simple block.
13#[derive(Clone, Copy, Eq, PartialEq)]
14pub enum SimpleBlockStyle {
15    /// A paragraph block with normal substitutions.
16    Paragraph,
17
18    /// A literal block with no substitutions.
19    Literal,
20
21    /// Blocks and paragraphs assigned the listing style display their rendered
22    /// content exactly as you see it in the source. Listing content is
23    /// converted to preformatted text (i.e., `<pre>`). The content is presented
24    /// in a fixed-width font and endlines are preserved. Only [special
25    /// characters] and callouts are replaced when the document is converted.
26    ///
27    /// [special characters]: https://docs.asciidoctor.org/asciidoc/latest/subs/special-characters/
28    Listing,
29
30    /// A source block is a specialization of a listing block. Developers are
31    /// accustomed to seeing source code colorized to emphasize the code’s
32    /// structure (i.e., keywords, types, delimiters, etc.).
33    Source,
34}
35
36impl std::fmt::Debug for SimpleBlockStyle {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            SimpleBlockStyle::Paragraph => write!(f, "SimpleBlockStyle::Paragraph"),
40            SimpleBlockStyle::Literal => write!(f, "SimpleBlockStyle::Literal"),
41            SimpleBlockStyle::Listing => write!(f, "SimpleBlockStyle::Listing"),
42            SimpleBlockStyle::Source => write!(f, "SimpleBlockStyle::Source"),
43        }
44    }
45}
46
47/// A block that's treated as contiguous lines of paragraph text (and subject to
48/// normal substitutions) (e.g., a paragraph block).
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct SimpleBlock<'src> {
51    content: Content<'src>,
52    source: Span<'src>,
53    style: SimpleBlockStyle,
54    title_source: Option<Span<'src>>,
55    title: Option<String>,
56    anchor: Option<Span<'src>>,
57    anchor_reftext: Option<Span<'src>>,
58    attrlist: Option<Attrlist<'src>>,
59}
60
61impl<'src> SimpleBlock<'src> {
62    pub(crate) fn parse(
63        metadata: &BlockMetadata<'src>,
64        parser: &mut Parser,
65    ) -> Option<MatchedItem<'src, Self>> {
66        let MatchedItem {
67            item: (content, style),
68            after,
69        } = parse_lines(metadata.block_start, &metadata.attrlist, parser)?;
70
71        Some(MatchedItem {
72            item: Self {
73                content,
74                source: metadata
75                    .source
76                    .trim_remainder(after)
77                    .trim_trailing_whitespace(),
78                style,
79                title_source: metadata.title_source,
80                title: metadata.title.clone(),
81                anchor: metadata.anchor,
82                anchor_reftext: metadata.anchor_reftext,
83                attrlist: metadata.attrlist.clone(),
84            },
85            after: after.discard_empty_lines(),
86        })
87    }
88
89    pub(crate) fn parse_fast(
90        source: Span<'src>,
91        parser: &Parser,
92    ) -> Option<MatchedItem<'src, Self>> {
93        let MatchedItem {
94            item: (content, style),
95            after,
96        } = parse_lines(source, &None, parser)?;
97
98        let source = content.original();
99
100        Some(MatchedItem {
101            item: Self {
102                content,
103                source,
104                style,
105                title_source: None,
106                title: None,
107                anchor: None,
108                anchor_reftext: None,
109                attrlist: None,
110            },
111            after: after.discard_empty_lines(),
112        })
113    }
114
115    /// Return the interpreted content of this block.
116    pub fn content(&self) -> &Content<'src> {
117        &self.content
118    }
119
120    /// Return the style of this block.
121    pub fn style(&self) -> SimpleBlockStyle {
122        self.style
123    }
124}
125
126/// Parse the content-bearing lines for this block.
127fn parse_lines<'src>(
128    source: Span<'src>,
129    attrlist: &Option<Attrlist<'src>>,
130    parser: &Parser,
131) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
132    let source_after_whitespace = source.discard_whitespace();
133    let strip_indent = source_after_whitespace.col() - 1;
134
135    let mut style = if source_after_whitespace.col() == source.col() {
136        SimpleBlockStyle::Paragraph
137    } else {
138        SimpleBlockStyle::Literal
139    };
140
141    // Block style can override the interpretation of literal from reading
142    // indentation.
143    if let Some(attrlist) = attrlist {
144        match attrlist.block_style() {
145            Some("normal") => {
146                style = SimpleBlockStyle::Paragraph;
147            }
148
149            Some("literal") => {
150                style = SimpleBlockStyle::Literal;
151            }
152
153            Some("listing") => {
154                style = SimpleBlockStyle::Listing;
155            }
156
157            Some("source") => {
158                style = SimpleBlockStyle::Source;
159            }
160
161            _ => {}
162        }
163    }
164
165    let mut next = source;
166    let mut filtered_lines: Vec<&'src str> = vec![];
167
168    while let Some(line_mi) = next.take_non_empty_line() {
169        let mut line = line_mi.item;
170
171        // There are several stop conditions for simple paragraph blocks. These
172        // "shouldn't" be encountered on the first line (we shouldn't be calling
173        // `SimpleBlock::parse` in these conditions), but in case it is, we simply
174        // ignore them on the first line.
175        if !filtered_lines.is_empty() {
176            if line.data() == "+" {
177                break;
178            }
179
180            if line.starts_with('[') && line.ends_with(']') {
181                break;
182            }
183
184            if (line.starts_with('/')
185                || line.starts_with('-')
186                || line.starts_with('.')
187                || line.starts_with('+')
188                || line.starts_with('=')
189                || line.starts_with('*')
190                || line.starts_with('_'))
191                && (RawDelimitedBlock::is_valid_delimiter(&line)
192                    || CompoundDelimitedBlock::is_valid_delimiter(&line))
193            {
194                break;
195            }
196        }
197
198        next = line_mi.after;
199
200        if line.starts_with("//") && !line.starts_with("///") {
201            continue;
202        }
203
204        // Strip at most the number of leading whitespace characters found on the first
205        // line.
206        if strip_indent > 0
207            && let Some(n) = line.position(|c| c != ' ' && c != '\t')
208        {
209            line = line.into_parse_result(n.min(strip_indent)).after;
210        };
211
212        filtered_lines.push(line.trim_trailing_whitespace().data());
213    }
214
215    let source = source.trim_remainder(next).trim_trailing_whitespace();
216    if source.is_empty() {
217        return None;
218    }
219
220    let filtered_lines = filtered_lines.join("\n");
221    let mut content: Content<'src> = Content::from_filtered(source, filtered_lines);
222
223    SubstitutionGroup::Normal
224        .override_via_attrlist(attrlist.as_ref())
225        .apply(&mut content, parser, attrlist.as_ref());
226
227    Some(MatchedItem {
228        item: (content, style),
229        after: next.discard_empty_lines(),
230    })
231}
232
233impl<'src> IsBlock<'src> for SimpleBlock<'src> {
234    fn content_model(&self) -> ContentModel {
235        ContentModel::Simple
236    }
237
238    fn rendered_content(&self) -> Option<&str> {
239        Some(self.content.rendered())
240    }
241
242    fn raw_context(&self) -> CowStr<'src> {
243        "paragraph".into()
244    }
245
246    fn title_source(&'src self) -> Option<Span<'src>> {
247        self.title_source
248    }
249
250    fn title(&self) -> Option<&str> {
251        self.title.as_deref()
252    }
253
254    fn anchor(&'src self) -> Option<Span<'src>> {
255        self.anchor
256    }
257
258    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
259        self.anchor_reftext
260    }
261
262    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
263        self.attrlist.as_ref()
264    }
265}
266
267impl<'src> HasSpan<'src> for SimpleBlock<'src> {
268    fn span(&self) -> Span<'src> {
269        self.source
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    #![allow(clippy::unwrap_used)]
276
277    use std::ops::Deref;
278
279    use pretty_assertions_sorted::assert_eq;
280
281    use crate::{
282        Parser,
283        blocks::{ContentModel, IsBlock, SimpleBlockStyle, metadata::BlockMetadata},
284        content::SubstitutionGroup,
285        tests::prelude::*,
286    };
287
288    #[test]
289    fn impl_clone() {
290        // Silly test to mark the #[derive(...)] line as covered.
291        let mut parser = Parser::default();
292
293        let b1 =
294            crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
295
296        let b2 = b1.item.clone();
297        assert_eq!(b1.item, b2);
298    }
299
300    #[test]
301    fn empty_source() {
302        let mut parser = Parser::default();
303        assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
304    }
305
306    #[test]
307    fn only_spaces() {
308        let mut parser = Parser::default();
309        assert!(
310            crate::blocks::SimpleBlock::parse(&BlockMetadata::new("    "), &mut parser).is_none()
311        );
312    }
313
314    #[test]
315    fn single_line() {
316        let mut parser = Parser::default();
317        let mi =
318            crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
319
320        assert_eq!(
321            mi.item,
322            SimpleBlock {
323                content: Content {
324                    original: Span {
325                        data: "abc",
326                        line: 1,
327                        col: 1,
328                        offset: 0,
329                    },
330                    rendered: "abc",
331                },
332                source: Span {
333                    data: "abc",
334                    line: 1,
335                    col: 1,
336                    offset: 0,
337                },
338                style: SimpleBlockStyle::Paragraph,
339                title_source: None,
340                title: None,
341                anchor: None,
342                anchor_reftext: None,
343                attrlist: None,
344            },
345        );
346
347        assert_eq!(mi.item.content_model(), ContentModel::Simple);
348        assert_eq!(mi.item.rendered_content().unwrap(), "abc");
349        assert_eq!(mi.item.raw_context().deref(), "paragraph");
350        assert_eq!(mi.item.resolved_context().deref(), "paragraph");
351        assert!(mi.item.declared_style().is_none());
352        assert!(mi.item.id().is_none());
353        assert!(mi.item.roles().is_empty());
354        assert!(mi.item.options().is_empty());
355        assert!(mi.item.title_source().is_none());
356        assert!(mi.item.title().is_none());
357        assert!(mi.item.anchor().is_none());
358        assert!(mi.item.anchor_reftext().is_none());
359        assert!(mi.item.attrlist().is_none());
360        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
361
362        assert_eq!(
363            mi.after,
364            Span {
365                data: "",
366                line: 1,
367                col: 4,
368                offset: 3
369            }
370        );
371    }
372
373    #[test]
374    fn multiple_lines() {
375        let mut parser = Parser::default();
376        let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
377            .unwrap();
378
379        assert_eq!(
380            mi.item,
381            SimpleBlock {
382                content: Content {
383                    original: Span {
384                        data: "abc\ndef",
385                        line: 1,
386                        col: 1,
387                        offset: 0,
388                    },
389                    rendered: "abc\ndef",
390                },
391                source: Span {
392                    data: "abc\ndef",
393                    line: 1,
394                    col: 1,
395                    offset: 0,
396                },
397                style: SimpleBlockStyle::Paragraph,
398                title_source: None,
399                title: None,
400                anchor: None,
401                anchor_reftext: None,
402                attrlist: None,
403            }
404        );
405
406        assert_eq!(
407            mi.after,
408            Span {
409                data: "",
410                line: 2,
411                col: 4,
412                offset: 7
413            }
414        );
415
416        assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
417    }
418
419    #[test]
420    fn consumes_blank_lines_after() {
421        let mut parser = Parser::default();
422        let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
423            .unwrap();
424
425        assert_eq!(
426            mi.item,
427            SimpleBlock {
428                content: Content {
429                    original: Span {
430                        data: "abc",
431                        line: 1,
432                        col: 1,
433                        offset: 0,
434                    },
435                    rendered: "abc",
436                },
437                source: Span {
438                    data: "abc",
439                    line: 1,
440                    col: 1,
441                    offset: 0,
442                },
443                style: SimpleBlockStyle::Paragraph,
444                title_source: None,
445                title: None,
446                anchor: None,
447                anchor_reftext: None,
448                attrlist: None,
449            }
450        );
451
452        assert_eq!(
453            mi.after,
454            Span {
455                data: "def",
456                line: 3,
457                col: 1,
458                offset: 5
459            }
460        );
461    }
462
463    #[test]
464    fn overrides_sub_group_via_subs_attribute() {
465        let mut parser = Parser::default();
466        let mi = crate::blocks::SimpleBlock::parse(
467            &BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
468            &mut parser,
469        )
470        .unwrap();
471
472        assert_eq!(
473            mi.item,
474            SimpleBlock {
475                content: Content {
476                    original: Span {
477                        data: "a<b>c *bold*",
478                        line: 2,
479                        col: 1,
480                        offset: 14,
481                    },
482                    rendered: "a<b>c <strong>bold</strong>",
483                },
484                source: Span {
485                    data: "[subs=quotes]\na<b>c *bold*",
486                    line: 1,
487                    col: 1,
488                    offset: 0,
489                },
490                style: SimpleBlockStyle::Paragraph,
491                title_source: None,
492                title: None,
493                anchor: None,
494                anchor_reftext: None,
495                attrlist: Some(Attrlist {
496                    attributes: &[ElementAttribute {
497                        name: Some("subs"),
498                        value: "quotes",
499                        shorthand_items: &[],
500                    },],
501                    anchor: None,
502                    source: Span {
503                        data: "subs=quotes",
504                        line: 1,
505                        col: 2,
506                        offset: 1,
507                    },
508                },),
509            }
510        );
511
512        assert_eq!(
513            mi.after,
514            Span {
515                data: "def",
516                line: 4,
517                col: 1,
518                offset: 28
519            }
520        );
521
522        assert_eq!(
523            mi.item.rendered_content().unwrap(),
524            "a<b>c <strong>bold</strong>"
525        );
526    }
527}