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