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