Skip to main content

asciidoc_parser/blocks/
raw_delimited.rs

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