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