Skip to main content

asciidoc_parser/blocks/
raw_delimited.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{ContentModel, IsBlock, caption::assign_block_caption, 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/// | `` ``` `` | Listing (fenced) |
21/// | `....`    | Literal      |
22/// | `++++`    | Passthrough  |
23///
24/// The fenced code block delimiter (three backticks) is a shorthand for a
25/// listing block. Like the open-block delimiter, it has a fixed length; four
26/// or more backticks are not a fence.
27///
28/// In addition, an open-block delimiter (`--`) is recognized here when it
29/// carries a verbatim masquerade style: `source` or `listing` (parsed as a
30/// listing block) or `literal` (parsed as a literal block). Every other open
31/// block is handled by
32/// [`CompoundDelimitedBlock`](crate::blocks::CompoundDelimitedBlock).
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct RawDelimitedBlock<'src> {
35    content: Content<'src>,
36    content_model: ContentModel,
37    context: CowStr<'src>,
38    source: Span<'src>,
39    title_source: Option<Span<'src>>,
40    title: Option<String>,
41    caption: Option<String>,
42    number: Option<usize>,
43    anchor: Option<Span<'src>>,
44    anchor_reftext: Option<Span<'src>>,
45    attrlist: Option<Attrlist<'src>>,
46    substitution_group: SubstitutionGroup,
47}
48
49impl<'src> RawDelimitedBlock<'src> {
50    pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
51        let data = line.data();
52
53        // The fenced code block delimiter is exactly three backticks. Unlike the
54        // four-character verbatim/raw delimiters, its length is fixed (as with
55        // the two-character open-block delimiter): a run of four or more
56        // backticks is not a fence.
57        if data == "```" {
58            return true;
59        }
60
61        // TO DO (https://github.com/asciidoc-rs/asciidoc-parser/issues/145):
62        // Seek spec clarity: Do the characters after the fourth char
63        // have to match the first four?
64
65        if data.len() >= 4 {
66            if data.starts_with("////") {
67                data.split_at(4).1.chars().all(|c| c == '/')
68            } else if data.starts_with("----") {
69                data.split_at(4).1.chars().all(|c| c == '-')
70            } else if data.starts_with("....") {
71                data.split_at(4).1.chars().all(|c| c == '.')
72            } else if data.starts_with("++++") {
73                data.split_at(4).1.chars().all(|c| c == '+')
74            } else {
75                false
76            }
77        } else {
78            false
79        }
80    }
81
82    pub(crate) fn parse(
83        metadata: &BlockMetadata<'src>,
84        parser: &mut Parser,
85    ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
86        let delimiter = metadata.block_start.take_normalized_line();
87        let delimiter_data = delimiter.item.data();
88
89        // A `--` open-block delimiter normally forms a compound (open) block, but
90        // a verbatim masquerade style (`source`, `listing`, or `literal`) set on
91        // it turns the block into a verbatim raw block. Every other delimiter
92        // must be at least four characters long.
93        let (content_model, context, mut substitution_group) = if delimiter_data == "--" {
94            // A plain or compound-styled open block returns `None` here and is
95            // handled by `CompoundDelimitedBlock` instead.
96            open_block_verbatim_masquerade(metadata.attrlist.as_ref())?
97        } else if delimiter_data == "```" {
98            // A fenced code block (three backticks) is a verbatim listing block.
99            // Its delimiter has a fixed length, so no trailing-character
100            // validity check is required. The closing fence must match the
101            // opening delimiter exactly, which the scan loop below enforces.
102            (
103                ContentModel::Verbatim,
104                "listing",
105                SubstitutionGroup::Verbatim,
106            )
107        } else {
108            if delimiter.item.len() < 4 {
109                return None;
110            }
111
112            let block_type = match delimiter_data
113                .split_at_checked(delimiter_data.len().min(4))?
114                .0
115            {
116                "////" => (ContentModel::Raw, "comment", SubstitutionGroup::None),
117                "----" => (
118                    ContentModel::Verbatim,
119                    "listing",
120                    SubstitutionGroup::Verbatim,
121                ),
122                "...." => (
123                    ContentModel::Verbatim,
124                    "literal",
125                    SubstitutionGroup::Verbatim,
126                ),
127                "++++" => pass_or_stem_block_type(metadata.attrlist.as_ref()),
128                _ => {
129                    return None;
130                }
131            };
132
133            // The four-character delimiters require a validity check (the
134            // trailing characters must match the first four); the `--` open
135            // delimiter is matched exactly above.
136            if !Self::is_valid_delimiter(&delimiter.item) {
137                return None;
138            }
139
140            block_type
141        };
142
143        // Assign the caption (and its number) from the block's context. Among
144        // the raw delimited contexts only `listing` is captionable (a `source`
145        // block resolves to the `listing` context); for every other context
146        // `assign_block_caption` returns `None`. The caption is computed once,
147        // here, so the context counter is consumed exactly once regardless of
148        // which return path the block takes below.
149        let caption = assign_block_caption(
150            parser,
151            context,
152            metadata.attrlist.as_ref(),
153            metadata.title.is_some(),
154        );
155        let number = caption.as_ref().and_then(|c| c.number);
156        let caption = caption.map(|c| c.prefix);
157
158        let content_start = delimiter.after;
159        let mut next = content_start;
160
161        while !next.is_empty() {
162            let line = next.take_normalized_line();
163            if line.item.data() == delimiter.item.data() {
164                let content = content_start.trim_remainder(next).trim_trailing_line_end();
165
166                let mut content: Content<'src> = content.into();
167
168                // A comment block (`////` or a `[comment]` open block) is never
169                // interpreted, so a `subs` attribute must not override its
170                // (empty) substitution group; every other raw context honors a
171                // `subs` override.
172                if context != "comment" {
173                    substitution_group =
174                        substitution_group.override_via_attrlist(metadata.attrlist.as_ref());
175                }
176
177                substitution_group.apply(&mut content, parser, metadata.attrlist.as_ref());
178
179                return Some(MatchAndWarnings {
180                    item: Some(MatchedItem {
181                        item: Self {
182                            content,
183                            content_model,
184                            context: context.into(),
185                            source: metadata
186                                .source
187                                .trim_remainder(line.after)
188                                .trim_trailing_line_end(),
189                            title_source: metadata.title_source,
190                            title: metadata.title.clone(),
191                            caption: caption.clone(),
192                            number,
193                            anchor: metadata.anchor,
194                            anchor_reftext: metadata.anchor_reftext,
195                            attrlist: metadata.attrlist.clone(),
196                            substitution_group,
197                        },
198                        after: line.after,
199                    }),
200                    warnings: vec![],
201                });
202            }
203
204            next = line.after;
205        }
206
207        let content = content_start.trim_remainder(next).trim_trailing_line_end();
208
209        Some(MatchAndWarnings {
210            item: Some(MatchedItem {
211                item: Self {
212                    content: content.into(),
213                    content_model,
214                    context: context.into(),
215                    source: metadata
216                        .source
217                        .trim_remainder(next)
218                        .trim_trailing_line_end(),
219                    title_source: metadata.title_source,
220                    title: metadata.title.clone(),
221                    caption,
222                    number,
223                    anchor: metadata.anchor,
224                    anchor_reftext: metadata.anchor_reftext,
225                    attrlist: metadata.attrlist.clone(),
226                    substitution_group,
227                },
228                after: next,
229            }),
230            warnings: vec![Warning {
231                source: delimiter.item,
232                warning: WarningType::UnterminatedDelimitedBlock,
233            }],
234        })
235    }
236
237    /// Return the interpreted content of this block.
238    pub fn content(&self) -> &Content<'src> {
239        &self.content
240    }
241}
242
243/// Resolve a raw or verbatim masquerade style set on an open block (`--`).
244///
245/// A block style replaces the open-block context only on an open block (every
246/// other delimited block keeps its own context). This parser claims an open
247/// block whose style turns it into a raw delimited block:
248///
249/// * the verbatim contexts — `source` and `listing` (both rendered as a listing
250///   block), or `literal` — make the open block a verbatim raw block;
251/// * the `pass` context makes the open block a passthrough (raw) block, whose
252///   content is emitted with no substitutions and no block parsing; and
253/// * the `comment` context makes the open block a comment block — the alternate
254///   open-block form of a `////` comment block. Its content is retained in the
255///   parsed model (this parser deliberately does not discard comments) but is
256///   raw: no substitutions are applied and no AsciiDoc syntax within it,
257///   including preprocessor directives, is interpreted.
258///
259/// Returns the resulting content model, context, and substitution group, or
260/// `None` when the style is absent or names a context that keeps the compound
261/// (open) content model (e.g. `sidebar`, `example`, `quote`), which is handled
262/// elsewhere.
263fn open_block_verbatim_masquerade(
264    attrlist: Option<&Attrlist<'_>>,
265) -> Option<(ContentModel, &'static str, SubstitutionGroup)> {
266    match attrlist?.block_style()? {
267        "source" | "listing" => Some((
268            ContentModel::Verbatim,
269            "listing",
270            SubstitutionGroup::Verbatim,
271        )),
272        "literal" => Some((
273            ContentModel::Verbatim,
274            "literal",
275            SubstitutionGroup::Verbatim,
276        )),
277        "pass" => Some((ContentModel::Raw, "pass", SubstitutionGroup::Pass)),
278        "comment" => Some((ContentModel::Raw, "comment", SubstitutionGroup::None)),
279        _ => None,
280    }
281}
282
283/// Resolve the content model, context, and substitution group for a passthrough
284/// (`++++`) delimited block.
285///
286/// A passthrough block normally has the `pass` context and applies no
287/// substitutions. When it carries a STEM style (`stem`, `asciimath`, or
288/// `latexmath`), it instead becomes a `stem` block: a raw block whose
289/// expression has only the special characters substitution applied (the
290/// notation's math delimiters are added by the converter at render time).
291fn pass_or_stem_block_type(
292    attrlist: Option<&Attrlist<'_>>,
293) -> (ContentModel, &'static str, SubstitutionGroup) {
294    match attrlist.and_then(|a| a.block_style()) {
295        Some("stem") | Some("asciimath") | Some("latexmath") => {
296            (ContentModel::Raw, "stem", SubstitutionGroup::Stem)
297        }
298        _ => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
299    }
300}
301
302impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
303    fn content_model(&self) -> ContentModel {
304        self.content_model
305    }
306
307    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
308        Some(&mut self.content)
309    }
310
311    fn rendered_content(&self) -> Option<&str> {
312        Some(self.content.rendered())
313    }
314
315    fn raw_context(&self) -> CowStr<'src> {
316        self.context.clone()
317    }
318
319    fn title_source(&'src self) -> Option<Span<'src>> {
320        self.title_source
321    }
322
323    fn title(&self) -> Option<&str> {
324        self.title.as_deref()
325    }
326
327    fn caption(&self) -> Option<&str> {
328        self.caption.as_deref()
329    }
330
331    fn number(&self) -> Option<usize> {
332        self.number
333    }
334
335    fn anchor(&'src self) -> Option<Span<'src>> {
336        self.anchor
337    }
338
339    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
340        self.anchor_reftext
341    }
342
343    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
344        self.attrlist.as_ref()
345    }
346
347    fn substitution_group(&'src self) -> SubstitutionGroup {
348        self.substitution_group.clone()
349    }
350}
351
352impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
353    fn span(&self) -> Span<'src> {
354        self.source
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    #![allow(clippy::unwrap_used)]
361
362    mod is_valid_delimiter {
363        use crate::blocks::RawDelimitedBlock;
364
365        #[test]
366        fn comment() {
367            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
368                "////"
369            )));
370            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
371                "/////"
372            )));
373            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
374                "/////////"
375            )));
376
377            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
378                "///"
379            )));
380            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
381                "//-/"
382            )));
383            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
384                "////-"
385            )));
386            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
387                "//////////x"
388            )));
389        }
390
391        #[test]
392        fn example() {
393            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
394                "===="
395            )));
396            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
397                "====="
398            )));
399            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
400                "==="
401            )));
402        }
403
404        #[test]
405        fn listing() {
406            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
407                "----"
408            )));
409            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
410                "-----"
411            )));
412            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
413                "---------"
414            )));
415
416            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
417                "---"
418            )));
419            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
420                "--/-"
421            )));
422            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
423                "----/"
424            )));
425            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
426                "----------x"
427            )));
428        }
429
430        #[test]
431        fn fenced() {
432            // The fenced code block delimiter is exactly three backticks.
433            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
434                "```"
435            )));
436
437            // A run of four or more backticks is not a fence (the delimiter has
438            // a fixed length, unlike the four-character delimiters).
439            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
440                "````"
441            )));
442            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
443                "``"
444            )));
445            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
446                "```java"
447            )));
448        }
449
450        #[test]
451        fn literal() {
452            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
453                "...."
454            )));
455            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
456                "....."
457            )));
458            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
459                "........."
460            )));
461
462            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
463                "..."
464            )));
465            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
466                "../."
467            )));
468            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
469                "..../"
470            )));
471            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
472                "..........x"
473            )));
474        }
475
476        #[test]
477        fn sidebar() {
478            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
479                "****"
480            )));
481            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
482                "*****"
483            )));
484            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
485                "***"
486            )));
487        }
488
489        #[test]
490        fn table() {
491            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
492                "|==="
493            )));
494            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
495                ",==="
496            )));
497            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
498                ":==="
499            )));
500            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
501                "!==="
502            )));
503        }
504
505        #[test]
506        fn pass() {
507            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
508                "++++"
509            )));
510            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
511                "+++++"
512            )));
513            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
514                "+++++++++"
515            )));
516
517            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
518                "+++"
519            )));
520            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
521                "++/+"
522            )));
523            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
524                "++++/"
525            )));
526            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
527                "++++++++++x"
528            )));
529        }
530
531        #[test]
532        fn quote() {
533            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
534                "____"
535            )));
536            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
537                "_____"
538            )));
539            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
540                "___"
541            )));
542        }
543    }
544
545    mod parse {
546        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
547
548        #[test]
549        fn err_invalid_delimiter() {
550            let mut parser = Parser::default();
551            assert!(
552                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
553                    .is_none()
554            );
555
556            let mut parser = Parser::default();
557            assert!(
558                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("..."), &mut parser)
559                    .is_none()
560            );
561
562            let mut parser = Parser::default();
563            assert!(
564                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("++++x"), &mut parser)
565                    .is_none()
566            );
567
568            let mut parser = Parser::default();
569            assert!(
570                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("____x"), &mut parser)
571                    .is_none()
572            );
573
574            let mut parser = Parser::default();
575            assert!(
576                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("====x"), &mut parser)
577                    .is_none()
578            );
579
580            let mut parser = Parser::default();
581            assert!(
582                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("==\n=="), &mut parser)
583                    .is_none()
584            );
585
586            let mut parser = Parser::default();
587            assert!(
588                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("===😀"), &mut parser)
589                    .is_none()
590            );
591        }
592
593        #[test]
594        fn err_unterminated() {
595            let mut parser = Parser::default();
596
597            let maw = crate::blocks::RawDelimitedBlock::parse(
598                &BlockMetadata::new("....\nblah blah blah"),
599                &mut parser,
600            )
601            .unwrap();
602
603            assert_eq!(
604                maw.warnings,
605                vec![Warning {
606                    source: Span {
607                        data: "....",
608                        line: 1,
609                        col: 1,
610                        offset: 0,
611                    },
612                    warning: WarningType::UnterminatedDelimitedBlock,
613                }]
614            );
615        }
616    }
617
618    mod comment {
619        use crate::{
620            blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
621            tests::prelude::*,
622        };
623
624        #[test]
625        fn empty() {
626            let mut parser = Parser::default();
627            let maw = crate::blocks::RawDelimitedBlock::parse(
628                &BlockMetadata::new("////\n////"),
629                &mut parser,
630            )
631            .unwrap();
632
633            let mi = maw.item.unwrap().clone();
634
635            assert_eq!(
636                mi.item,
637                RawDelimitedBlock {
638                    content: Content {
639                        original: Span {
640                            data: "",
641                            line: 2,
642                            col: 1,
643                            offset: 5,
644                        },
645                        rendered: "",
646                    },
647                    content_model: ContentModel::Raw,
648                    context: "comment",
649                    source: Span {
650                        data: "////\n////",
651                        line: 1,
652                        col: 1,
653                        offset: 0,
654                    },
655                    title_source: None,
656                    title: None,
657                    caption: None,
658                    number: None,
659                    anchor: None,
660                    anchor_reftext: None,
661                    attrlist: None,
662                    substitution_group: SubstitutionGroup::None,
663                }
664            );
665
666            assert_eq!(mi.item.content_model(), ContentModel::Raw);
667            assert_eq!(mi.item.rendered_content().unwrap(), "");
668            assert_eq!(mi.item.raw_context().as_ref(), "comment");
669            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
670            assert!(mi.item.declared_style().is_none());
671            assert!(mi.item.content().is_empty());
672            assert!(mi.item.id().is_none());
673            assert!(mi.item.roles().is_empty());
674            assert!(mi.item.options().is_empty());
675            assert!(mi.item.title_source().is_none());
676            assert!(mi.item.title().is_none());
677            assert!(mi.item.anchor().is_none());
678            assert!(mi.item.anchor_reftext().is_none());
679            assert!(mi.item.attrlist().is_none());
680            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
681        }
682
683        #[test]
684        fn multiple_lines() {
685            let mut parser = Parser::default();
686
687            let maw = crate::blocks::RawDelimitedBlock::parse(
688                &BlockMetadata::new("////\nline1  \nline2\n////"),
689                &mut parser,
690            )
691            .unwrap();
692
693            let mi = maw.item.unwrap().clone();
694
695            assert_eq!(
696                mi.item,
697                RawDelimitedBlock {
698                    content: Content {
699                        original: Span {
700                            data: "line1  \nline2",
701                            line: 2,
702                            col: 1,
703                            offset: 5,
704                        },
705                        rendered: "line1  \nline2",
706                    },
707                    content_model: ContentModel::Raw,
708                    context: "comment",
709                    source: Span {
710                        data: "////\nline1  \nline2\n////",
711                        line: 1,
712                        col: 1,
713                        offset: 0,
714                    },
715                    title_source: None,
716                    title: None,
717                    caption: None,
718                    number: None,
719                    anchor: None,
720                    anchor_reftext: None,
721                    attrlist: None,
722                    substitution_group: SubstitutionGroup::None,
723                }
724            );
725
726            assert_eq!(mi.item.content_model(), ContentModel::Raw);
727            assert_eq!(mi.item.rendered_content().unwrap(), "line1  \nline2");
728            assert_eq!(mi.item.raw_context().as_ref(), "comment");
729            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
730            assert!(mi.item.declared_style().is_none());
731            assert!(mi.item.id().is_none());
732            assert!(mi.item.roles().is_empty());
733            assert!(mi.item.options().is_empty());
734            assert!(mi.item.title_source().is_none());
735            assert!(mi.item.title().is_none());
736            assert!(mi.item.anchor().is_none());
737            assert!(mi.item.anchor_reftext().is_none());
738            assert!(mi.item.attrlist().is_none());
739            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
740
741            assert_eq!(
742                mi.item.content(),
743                Content {
744                    original: Span {
745                        data: "line1  \nline2",
746                        line: 2,
747                        col: 1,
748                        offset: 5,
749                    },
750                    rendered: "line1  \nline2",
751                }
752            );
753        }
754
755        #[test]
756        fn ignores_delimiter_prefix() {
757            let mut parser = Parser::default();
758
759            let maw = crate::blocks::RawDelimitedBlock::parse(
760                &BlockMetadata::new("////\nline1  \n/////\nline2\n////"),
761                &mut parser,
762            )
763            .unwrap();
764
765            let mi = maw.item.unwrap().clone();
766
767            assert_eq!(
768                mi.item,
769                RawDelimitedBlock {
770                    content: Content {
771                        original: Span {
772                            data: "line1  \n/////\nline2",
773                            line: 2,
774                            col: 1,
775                            offset: 5,
776                        },
777                        rendered: "line1  \n/////\nline2",
778                    },
779                    content_model: ContentModel::Raw,
780                    context: "comment",
781                    source: Span {
782                        data: "////\nline1  \n/////\nline2\n////",
783                        line: 1,
784                        col: 1,
785                        offset: 0,
786                    },
787                    title_source: None,
788                    title: None,
789                    caption: None,
790                    number: None,
791                    anchor: None,
792                    anchor_reftext: None,
793                    attrlist: None,
794                    substitution_group: SubstitutionGroup::None,
795                }
796            );
797
798            assert_eq!(mi.item.content_model(), ContentModel::Raw);
799            assert_eq!(mi.item.raw_context().as_ref(), "comment");
800            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
801            assert!(mi.item.declared_style().is_none());
802            assert!(mi.item.id().is_none());
803            assert!(mi.item.roles().is_empty());
804            assert!(mi.item.options().is_empty());
805            assert!(mi.item.title_source().is_none());
806            assert!(mi.item.title().is_none());
807            assert!(mi.item.anchor().is_none());
808            assert!(mi.item.anchor_reftext().is_none());
809            assert!(mi.item.attrlist().is_none());
810            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
811
812            assert_eq!(
813                mi.item.content(),
814                Content {
815                    original: Span {
816                        data: "line1  \n/////\nline2",
817                        line: 2,
818                        col: 1,
819                        offset: 5,
820                    },
821                    rendered: "line1  \n/////\nline2",
822                }
823            );
824        }
825
826        #[test]
827        fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
828            let mut parser = Parser::default();
829            assert!(
830                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("///😀"), &mut parser)
831                    .is_none()
832            );
833        }
834    }
835
836    mod example {
837        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
838
839        #[test]
840        fn empty() {
841            let mut parser = Parser::default();
842            assert!(
843                crate::blocks::RawDelimitedBlock::parse(
844                    &BlockMetadata::new("====\n===="),
845                    &mut parser
846                )
847                .is_none()
848            );
849        }
850
851        #[test]
852        fn multiple_lines() {
853            let mut parser = Parser::default();
854            assert!(
855                crate::blocks::RawDelimitedBlock::parse(
856                    &BlockMetadata::new("====\nline1  \nline2\n===="),
857                    &mut parser
858                )
859                .is_none()
860            );
861        }
862    }
863
864    mod listing {
865        use crate::{
866            blocks::{ContentModel, metadata::BlockMetadata},
867            content::SubstitutionStep,
868            tests::prelude::*,
869        };
870
871        #[test]
872        fn empty() {
873            let mut parser = Parser::default();
874
875            let maw = crate::blocks::RawDelimitedBlock::parse(
876                &BlockMetadata::new("----\n----"),
877                &mut parser,
878            )
879            .unwrap();
880
881            let mi = maw.item.unwrap().clone();
882
883            assert_eq!(
884                mi.item,
885                RawDelimitedBlock {
886                    content: Content {
887                        original: Span {
888                            data: "",
889                            line: 2,
890                            col: 1,
891                            offset: 5,
892                        },
893                        rendered: "",
894                    },
895                    content_model: ContentModel::Verbatim,
896                    context: "listing",
897                    source: Span {
898                        data: "----\n----",
899                        line: 1,
900                        col: 1,
901                        offset: 0,
902                    },
903                    title_source: None,
904                    title: None,
905                    caption: None,
906                    number: None,
907                    anchor: None,
908                    anchor_reftext: None,
909                    attrlist: None,
910                    substitution_group: SubstitutionGroup::Verbatim,
911                }
912            );
913
914            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
915            assert_eq!(mi.item.raw_context().as_ref(), "listing");
916            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
917            assert!(mi.item.declared_style().is_none());
918            assert!(mi.item.content().is_empty());
919            assert!(mi.item.id().is_none());
920            assert!(mi.item.roles().is_empty());
921            assert!(mi.item.options().is_empty());
922            assert!(mi.item.title_source().is_none());
923            assert!(mi.item.title().is_none());
924            assert!(mi.item.anchor().is_none());
925            assert!(mi.item.anchor_reftext().is_none());
926            assert!(mi.item.attrlist().is_none());
927            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
928        }
929
930        #[test]
931        fn multiple_lines() {
932            let mut parser = Parser::default();
933
934            let maw = crate::blocks::RawDelimitedBlock::parse(
935                &BlockMetadata::new("----\nline1  \nline2\n----"),
936                &mut parser,
937            )
938            .unwrap();
939
940            let mi = maw.item.unwrap().clone();
941
942            assert_eq!(
943                mi.item,
944                RawDelimitedBlock {
945                    content: Content {
946                        original: Span {
947                            data: "line1  \nline2",
948                            line: 2,
949                            col: 1,
950                            offset: 5,
951                        },
952                        rendered: "line1  \nline2",
953                    },
954                    content_model: ContentModel::Verbatim,
955                    context: "listing",
956                    source: Span {
957                        data: "----\nline1  \nline2\n----",
958                        line: 1,
959                        col: 1,
960                        offset: 0,
961                    },
962                    title_source: None,
963                    title: None,
964                    caption: None,
965                    number: None,
966                    anchor: None,
967                    anchor_reftext: None,
968                    attrlist: None,
969                    substitution_group: SubstitutionGroup::Verbatim,
970                }
971            );
972
973            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
974            assert_eq!(mi.item.raw_context().as_ref(), "listing");
975            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
976            assert!(mi.item.declared_style().is_none());
977            assert!(mi.item.id().is_none());
978            assert!(mi.item.roles().is_empty());
979            assert!(mi.item.options().is_empty());
980            assert!(mi.item.title_source().is_none());
981            assert!(mi.item.title().is_none());
982            assert!(mi.item.anchor().is_none());
983            assert!(mi.item.anchor_reftext().is_none());
984            assert!(mi.item.attrlist().is_none());
985            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
986
987            assert_eq!(
988                mi.item.content(),
989                Content {
990                    original: Span {
991                        data: "line1  \nline2",
992                        line: 2,
993                        col: 1,
994                        offset: 5,
995                    },
996                    rendered: "line1  \nline2",
997                }
998            );
999        }
1000
1001        #[test]
1002        fn overrides_sub_group_via_subs_attribute() {
1003            let mut parser = Parser::default();
1004
1005            let maw = crate::blocks::RawDelimitedBlock::parse(
1006                &BlockMetadata::new("[subs=quotes]\n----\nline1 < *line2*\n----"),
1007                &mut parser,
1008            )
1009            .unwrap();
1010
1011            let mi = maw.item.unwrap().clone();
1012
1013            assert_eq!(
1014                mi.item,
1015                RawDelimitedBlock {
1016                    content: Content {
1017                        original: Span {
1018                            data: "line1 < *line2*",
1019                            line: 3,
1020                            col: 1,
1021                            offset: 19,
1022                        },
1023                        rendered: "line1 < <strong>line2</strong>",
1024                    },
1025                    content_model: ContentModel::Verbatim,
1026                    context: "listing",
1027                    source: Span {
1028                        data: "[subs=quotes]\n----\nline1 < *line2*\n----",
1029                        line: 1,
1030                        col: 1,
1031                        offset: 0,
1032                    },
1033                    title_source: None,
1034                    title: None,
1035                    caption: None,
1036                    number: None,
1037                    anchor: None,
1038                    anchor_reftext: None,
1039                    attrlist: Some(Attrlist {
1040                        attributes: &[ElementAttribute {
1041                            name: Some("subs"),
1042                            value: "quotes",
1043                            shorthand_items: &[],
1044                        },],
1045                        anchor: None,
1046                        source: Span {
1047                            data: "subs=quotes",
1048                            line: 1,
1049                            col: 2,
1050                            offset: 1,
1051                        },
1052                    },),
1053                    substitution_group: SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
1054                }
1055            );
1056
1057            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1058            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1059            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1060            assert!(mi.item.declared_style().is_none());
1061            assert!(mi.item.id().is_none());
1062            assert!(mi.item.roles().is_empty());
1063            assert!(mi.item.options().is_empty());
1064            assert!(mi.item.title_source().is_none());
1065            assert!(mi.item.title().is_none());
1066            assert!(mi.item.anchor().is_none());
1067            assert!(mi.item.anchor_reftext().is_none());
1068
1069            assert_eq!(
1070                mi.item.attrlist().unwrap(),
1071                Attrlist {
1072                    attributes: &[ElementAttribute {
1073                        name: Some("subs"),
1074                        value: "quotes",
1075                        shorthand_items: &[],
1076                    },],
1077                    anchor: None,
1078                    source: Span {
1079                        data: "subs=quotes",
1080                        line: 1,
1081                        col: 2,
1082                        offset: 1,
1083                    },
1084                }
1085            );
1086
1087            assert_eq!(
1088                mi.item.substitution_group(),
1089                SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes])
1090            );
1091
1092            assert_eq!(
1093                mi.item.content(),
1094                Content {
1095                    original: Span {
1096                        data: "line1 < *line2*",
1097                        line: 3,
1098                        col: 1,
1099                        offset: 19,
1100                    },
1101                    rendered: "line1 < <strong>line2</strong>",
1102                }
1103            );
1104        }
1105
1106        #[test]
1107        fn ignores_delimiter_prefix() {
1108            let mut parser = Parser::default();
1109
1110            let maw = crate::blocks::RawDelimitedBlock::parse(
1111                &BlockMetadata::new("----\nline1  \n-----\nline2\n----"),
1112                &mut parser,
1113            )
1114            .unwrap();
1115
1116            let mi = maw.item.unwrap().clone();
1117
1118            assert_eq!(
1119                mi.item,
1120                RawDelimitedBlock {
1121                    content: Content {
1122                        original: Span {
1123                            data: "line1  \n-----\nline2",
1124                            line: 2,
1125                            col: 1,
1126                            offset: 5,
1127                        },
1128                        rendered: "line1  \n-----\nline2",
1129                    },
1130                    content_model: ContentModel::Verbatim,
1131                    context: "listing",
1132                    source: Span {
1133                        data: "----\nline1  \n-----\nline2\n----",
1134                        line: 1,
1135                        col: 1,
1136                        offset: 0,
1137                    },
1138                    title_source: None,
1139                    title: None,
1140                    caption: None,
1141                    number: None,
1142                    anchor: None,
1143                    anchor_reftext: None,
1144                    attrlist: None,
1145                    substitution_group: SubstitutionGroup::Verbatim,
1146                }
1147            );
1148
1149            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1150            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1151            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1152            assert!(mi.item.declared_style().is_none());
1153            assert!(mi.item.id().is_none());
1154            assert!(mi.item.roles().is_empty());
1155            assert!(mi.item.options().is_empty());
1156            assert!(mi.item.title_source().is_none());
1157            assert!(mi.item.title().is_none());
1158            assert!(mi.item.anchor().is_none());
1159            assert!(mi.item.anchor_reftext().is_none());
1160            assert!(mi.item.attrlist().is_none());
1161            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1162
1163            assert_eq!(
1164                mi.item.content(),
1165                Content {
1166                    original: Span {
1167                        data: "line1  \n-----\nline2",
1168                        line: 2,
1169                        col: 1,
1170                        offset: 5,
1171                    },
1172                    rendered: "line1  \n-----\nline2",
1173                }
1174            );
1175
1176            assert_eq!(
1177                mi.item.content(),
1178                Content {
1179                    original: Span {
1180                        data: "line1  \n-----\nline2",
1181                        line: 2,
1182                        col: 1,
1183                        offset: 5,
1184                    },
1185                    rendered: "line1  \n-----\nline2",
1186                }
1187            );
1188        }
1189    }
1190
1191    mod sidebar {
1192        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1193
1194        #[test]
1195        fn empty() {
1196            let mut parser = Parser::default();
1197            assert!(
1198                crate::blocks::RawDelimitedBlock::parse(
1199                    &BlockMetadata::new("****\n****"),
1200                    &mut parser
1201                )
1202                .is_none()
1203            );
1204        }
1205
1206        #[test]
1207        fn multiple_lines() {
1208            let mut parser = Parser::default();
1209            assert!(
1210                crate::blocks::RawDelimitedBlock::parse(
1211                    &BlockMetadata::new("****\nline1  \nline2\n****"),
1212                    &mut parser
1213                )
1214                .is_none()
1215            );
1216        }
1217    }
1218
1219    mod table {
1220        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1221
1222        #[test]
1223        fn empty() {
1224            let mut parser = Parser::default();
1225            assert!(
1226                crate::blocks::RawDelimitedBlock::parse(
1227                    &BlockMetadata::new("|===\n|==="),
1228                    &mut parser
1229                )
1230                .is_none()
1231            );
1232
1233            let mut parser = Parser::default();
1234            assert!(
1235                crate::blocks::RawDelimitedBlock::parse(
1236                    &BlockMetadata::new(",===\n,==="),
1237                    &mut parser
1238                )
1239                .is_none()
1240            );
1241
1242            let mut parser = Parser::default();
1243            assert!(
1244                crate::blocks::RawDelimitedBlock::parse(
1245                    &BlockMetadata::new(":===\n:==="),
1246                    &mut parser
1247                )
1248                .is_none()
1249            );
1250
1251            let mut parser = Parser::default();
1252            assert!(
1253                crate::blocks::RawDelimitedBlock::parse(
1254                    &BlockMetadata::new("!===\n!==="),
1255                    &mut parser
1256                )
1257                .is_none()
1258            );
1259        }
1260
1261        #[test]
1262        fn multiple_lines() {
1263            let mut parser = Parser::default();
1264            assert!(
1265                crate::blocks::RawDelimitedBlock::parse(
1266                    &BlockMetadata::new("|===\nline1  \nline2\n|==="),
1267                    &mut parser
1268                )
1269                .is_none()
1270            );
1271
1272            let mut parser = Parser::default();
1273            assert!(
1274                crate::blocks::RawDelimitedBlock::parse(
1275                    &BlockMetadata::new(",===\nline1  \nline2\n,==="),
1276                    &mut parser
1277                )
1278                .is_none()
1279            );
1280
1281            let mut parser = Parser::default();
1282            assert!(
1283                crate::blocks::RawDelimitedBlock::parse(
1284                    &BlockMetadata::new(":===\nline1  \nline2\n:==="),
1285                    &mut parser
1286                )
1287                .is_none()
1288            );
1289
1290            let mut parser = Parser::default();
1291            assert!(
1292                crate::blocks::RawDelimitedBlock::parse(
1293                    &BlockMetadata::new("!===\nline1  \nline2\n!==="),
1294                    &mut parser
1295                )
1296                .is_none()
1297            );
1298        }
1299    }
1300
1301    mod pass {
1302        use crate::{
1303            blocks::{ContentModel, metadata::BlockMetadata},
1304            tests::prelude::*,
1305        };
1306
1307        #[test]
1308        fn empty() {
1309            let mut parser = Parser::default();
1310            let maw = crate::blocks::RawDelimitedBlock::parse(
1311                &BlockMetadata::new("++++\n++++"),
1312                &mut parser,
1313            )
1314            .unwrap();
1315
1316            let mi = maw.item.unwrap().clone();
1317
1318            assert_eq!(
1319                mi.item,
1320                RawDelimitedBlock {
1321                    content: Content {
1322                        original: Span {
1323                            data: "",
1324                            line: 2,
1325                            col: 1,
1326                            offset: 5,
1327                        },
1328                        rendered: "",
1329                    },
1330                    content_model: ContentModel::Raw,
1331                    context: "pass",
1332                    source: Span {
1333                        data: "++++\n++++",
1334                        line: 1,
1335                        col: 1,
1336                        offset: 0,
1337                    },
1338                    title_source: None,
1339                    title: None,
1340                    caption: None,
1341                    number: None,
1342                    anchor: None,
1343                    anchor_reftext: None,
1344                    attrlist: None,
1345                    substitution_group: SubstitutionGroup::Pass,
1346                }
1347            );
1348
1349            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1350            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1351            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1352            assert!(mi.item.declared_style().is_none());
1353            assert!(mi.item.content().is_empty());
1354            assert!(mi.item.id().is_none());
1355            assert!(mi.item.roles().is_empty());
1356            assert!(mi.item.options().is_empty());
1357            assert!(mi.item.title_source().is_none());
1358            assert!(mi.item.title().is_none());
1359            assert!(mi.item.anchor().is_none());
1360            assert!(mi.item.anchor_reftext().is_none());
1361            assert!(mi.item.attrlist().is_none());
1362            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1363        }
1364
1365        #[test]
1366        fn multiple_lines() {
1367            let mut parser = Parser::default();
1368
1369            let maw = crate::blocks::RawDelimitedBlock::parse(
1370                &BlockMetadata::new("++++\nline1  \nline2\n++++"),
1371                &mut parser,
1372            )
1373            .unwrap();
1374
1375            let mi = maw.item.unwrap().clone();
1376
1377            assert_eq!(
1378                mi.item,
1379                RawDelimitedBlock {
1380                    content: Content {
1381                        original: Span {
1382                            data: "line1  \nline2",
1383                            line: 2,
1384                            col: 1,
1385                            offset: 5,
1386                        },
1387                        rendered: "line1  \nline2",
1388                    },
1389                    content_model: ContentModel::Raw,
1390                    context: "pass",
1391                    source: Span {
1392                        data: "++++\nline1  \nline2\n++++",
1393                        line: 1,
1394                        col: 1,
1395                        offset: 0,
1396                    },
1397                    title_source: None,
1398                    title: None,
1399                    caption: None,
1400                    number: None,
1401                    anchor: None,
1402                    anchor_reftext: None,
1403                    attrlist: None,
1404                    substitution_group: SubstitutionGroup::Pass,
1405                }
1406            );
1407
1408            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1409            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1410            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1411            assert!(mi.item.declared_style().is_none());
1412            assert!(mi.item.id().is_none());
1413            assert!(mi.item.roles().is_empty());
1414            assert!(mi.item.options().is_empty());
1415            assert!(mi.item.title_source().is_none());
1416            assert!(mi.item.title().is_none());
1417            assert!(mi.item.anchor().is_none());
1418            assert!(mi.item.anchor_reftext().is_none());
1419            assert!(mi.item.attrlist().is_none());
1420            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1421
1422            assert_eq!(
1423                mi.item.content(),
1424                Content {
1425                    original: Span {
1426                        data: "line1  \nline2",
1427                        line: 2,
1428                        col: 1,
1429                        offset: 5,
1430                    },
1431                    rendered: "line1  \nline2",
1432                }
1433            );
1434        }
1435
1436        #[test]
1437        fn ignores_delimiter_prefix() {
1438            let mut parser = Parser::default();
1439
1440            let maw = crate::blocks::RawDelimitedBlock::parse(
1441                &BlockMetadata::new("++++\nline1  \n+++++\nline2\n++++"),
1442                &mut parser,
1443            )
1444            .unwrap();
1445
1446            let mi = maw.item.unwrap().clone();
1447
1448            assert_eq!(
1449                mi.item,
1450                RawDelimitedBlock {
1451                    content: Content {
1452                        original: Span {
1453                            data: "line1  \n+++++\nline2",
1454                            line: 2,
1455                            col: 1,
1456                            offset: 5,
1457                        },
1458                        rendered: "line1  \n+++++\nline2",
1459                    },
1460                    content_model: ContentModel::Raw,
1461                    context: "pass",
1462                    source: Span {
1463                        data: "++++\nline1  \n+++++\nline2\n++++",
1464                        line: 1,
1465                        col: 1,
1466                        offset: 0,
1467                    },
1468                    title_source: None,
1469                    title: None,
1470                    caption: None,
1471                    number: None,
1472                    anchor: None,
1473                    anchor_reftext: None,
1474                    attrlist: None,
1475                    substitution_group: SubstitutionGroup::Pass,
1476                }
1477            );
1478
1479            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1480            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1481            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1482            assert!(mi.item.declared_style().is_none());
1483            assert!(mi.item.id().is_none());
1484            assert!(mi.item.roles().is_empty());
1485            assert!(mi.item.options().is_empty());
1486            assert!(mi.item.title_source().is_none());
1487            assert!(mi.item.title().is_none());
1488            assert!(mi.item.anchor().is_none());
1489            assert!(mi.item.anchor_reftext().is_none());
1490            assert!(mi.item.attrlist().is_none());
1491            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1492
1493            assert_eq!(
1494                mi.item.content(),
1495                Content {
1496                    original: Span {
1497                        data: "line1  \n+++++\nline2",
1498                        line: 2,
1499                        col: 1,
1500                        offset: 5,
1501                    },
1502                    rendered: "line1  \n+++++\nline2",
1503                }
1504            );
1505        }
1506    }
1507
1508    mod stem {
1509        use crate::{blocks::ContentModel, tests::prelude::*};
1510
1511        /// A `[stem]` passthrough block becomes a `stem` block whose expression
1512        /// has only the special characters substitution applied. The notation's
1513        /// math delimiters are added by the converter at render time, so they
1514        /// do not appear in the parsed content.
1515        #[test]
1516        fn stem_style_block() {
1517            let doc = Parser::default().parse("[stem]\n++++\na < b\n++++");
1518            let block = doc.nested_blocks().next().unwrap();
1519
1520            assert_eq!(block.content_model(), ContentModel::Raw);
1521            assert_eq!(block.raw_context().as_ref(), "stem");
1522            assert_eq!(block.resolved_context().as_ref(), "stem");
1523            assert_eq!(block.declared_style(), Some("stem"));
1524            assert_eq!(block.rendered_content(), Some("a &lt; b"));
1525            assert_eq!(block.substitution_group(), SubstitutionGroup::Stem);
1526            assert!(doc.warnings().next().is_none());
1527        }
1528
1529        #[test]
1530        fn asciimath_style_block() {
1531            let doc = Parser::default().parse("[asciimath]\n++++\nx^2\n++++");
1532            let block = doc.nested_blocks().next().unwrap();
1533
1534            assert_eq!(block.raw_context().as_ref(), "stem");
1535            assert_eq!(block.declared_style(), Some("asciimath"));
1536            assert_eq!(block.rendered_content(), Some("x^2"));
1537        }
1538
1539        #[test]
1540        fn latexmath_style_block() {
1541            let doc = Parser::default().parse("[latexmath]\n++++\nC = \\alpha\n++++");
1542            let block = doc.nested_blocks().next().unwrap();
1543
1544            assert_eq!(block.raw_context().as_ref(), "stem");
1545            assert_eq!(block.declared_style(), Some("latexmath"));
1546            assert_eq!(block.rendered_content(), Some(r"C = \alpha"));
1547        }
1548
1549        /// Without a STEM style, a `++++` block remains a `pass` block with no
1550        /// substitutions applied.
1551        #[test]
1552        fn unstyled_block_is_still_pass() {
1553            let doc = Parser::default().parse("++++\na < b\n++++");
1554            let block = doc.nested_blocks().next().unwrap();
1555
1556            assert_eq!(block.raw_context().as_ref(), "pass");
1557            assert_eq!(block.rendered_content(), Some("a < b"));
1558            assert_eq!(block.substitution_group(), SubstitutionGroup::Pass);
1559        }
1560
1561        /// An explicit `subs` attribute still overrides a STEM block's default
1562        /// substitution group.
1563        #[test]
1564        fn subs_attribute_overrides_stem_default() {
1565            let doc = Parser::default().parse("[stem,subs=none]\n++++\na < b\n++++");
1566            let block = doc.nested_blocks().next().unwrap();
1567
1568            assert_eq!(block.raw_context().as_ref(), "stem");
1569            assert_eq!(block.rendered_content(), Some("a < b"));
1570            assert_eq!(block.substitution_group(), SubstitutionGroup::None);
1571        }
1572    }
1573
1574    mod quote {
1575        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1576
1577        #[test]
1578        fn empty() {
1579            let mut parser = Parser::default();
1580            assert!(
1581                crate::blocks::RawDelimitedBlock::parse(
1582                    &BlockMetadata::new("____\n____"),
1583                    &mut parser
1584                )
1585                .is_none()
1586            );
1587        }
1588
1589        #[test]
1590        fn multiple_lines() {
1591            let mut parser = Parser::default();
1592            assert!(
1593                crate::blocks::RawDelimitedBlock::parse(
1594                    &BlockMetadata::new("____\nline1  \nline2\n____"),
1595                    &mut parser
1596                )
1597                .is_none()
1598            );
1599        }
1600    }
1601}