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, 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 {
291                source: delimiter.item,
292                warning: WarningType::UnterminatedDelimitedBlock,
293                origin: None,
294            }],
295        })
296    }
297
298    /// Return the interpreted content of this block.
299    pub fn content(&self) -> &Content<'src> {
300        &self.content
301    }
302}
303
304/// If `line` opens a language-aware fenced code block, return the language
305/// declared on the opening fence.
306///
307/// A language-aware fence is exactly three backticks immediately followed by an
308/// info string (`` ```ruby ``). The language is the first whitespace-delimited
309/// token of that info string. A bare `` ``` `` fence, a run of four or more
310/// backticks, and any non-fence line all return `None`.
311fn fenced_code_language<'src>(line: &Span<'src>) -> Option<Span<'src>> {
312    let rest = line.data().strip_prefix("```")?;
313
314    // A bare fence carries no language; a fourth backtick makes this a longer
315    // run, which is not a fence.
316    if rest.is_empty() || rest.starts_with('`') {
317        return None;
318    }
319
320    // The language is the first whitespace-delimited token of the info string
321    // (any leading whitespace is skipped; trailing content is ignored).
322    let info = line.discard(3).take_whitespace().after;
323    let language = info.take_while(|c| c != ' ' && c != '\t').item;
324
325    if language.is_empty() {
326        None
327    } else {
328        Some(language)
329    }
330}
331
332/// Resolve a raw or verbatim masquerade style set on an open block (`--`).
333///
334/// A block style replaces the open-block context only on an open block (every
335/// other delimited block keeps its own context). This parser claims an open
336/// block whose style turns it into a raw delimited block:
337///
338/// * the verbatim contexts – `source` and `listing` (both rendered as a listing
339///   block), or `literal` – make the open block a verbatim raw block;
340/// * the `pass` context makes the open block a passthrough (raw) block, whose
341///   content is emitted with no substitutions and no block parsing; and
342/// * the `comment` context makes the open block a comment block – the alternate
343///   open-block form of a `////` comment block. Its content is retained in the
344///   parsed model (this parser deliberately does not discard comments) but is
345///   raw: no substitutions are applied and no AsciiDoc syntax within it,
346///   including preprocessor directives, is interpreted.
347///
348/// Returns the resulting content model, context, and substitution group, or
349/// `None` when the style is absent or names a context that keeps the compound
350/// (open) content model (e.g. `sidebar`, `example`, `quote`), which is handled
351/// elsewhere.
352fn open_block_verbatim_masquerade(
353    attrlist: Option<&Attrlist<'_>>,
354) -> Option<(ContentModel, &'static str, SubstitutionGroup)> {
355    match attrlist?.block_style()? {
356        "source" | "listing" => Some((
357            ContentModel::Verbatim,
358            "listing",
359            SubstitutionGroup::Verbatim,
360        )),
361        "literal" => Some((
362            ContentModel::Verbatim,
363            "literal",
364            SubstitutionGroup::Verbatim,
365        )),
366        "pass" => Some((ContentModel::Raw, "pass", SubstitutionGroup::Pass)),
367        "comment" => Some((ContentModel::Raw, "comment", SubstitutionGroup::None)),
368        _ => None,
369    }
370}
371
372/// Resolve the content model, context, and substitution group for a passthrough
373/// (`++++`) delimited block.
374///
375/// A passthrough block normally has the `pass` context and applies no
376/// substitutions. When it carries a STEM style (`stem`, `asciimath`, or
377/// `latexmath`), it instead becomes a `stem` block: a raw block whose
378/// expression has only the special characters substitution applied (the
379/// notation's math delimiters are added by the converter at render time).
380fn pass_or_stem_block_type(
381    attrlist: Option<&Attrlist<'_>>,
382) -> (ContentModel, &'static str, SubstitutionGroup) {
383    match attrlist.and_then(|a| a.block_style()) {
384        Some("stem") | Some("asciimath") | Some("latexmath") => {
385            (ContentModel::Raw, "stem", SubstitutionGroup::Stem)
386        }
387        _ => (ContentModel::Raw, "pass", SubstitutionGroup::Pass),
388    }
389}
390
391impl<'src> IsBlock<'src> for RawDelimitedBlock<'src> {
392    fn content_model(&self) -> ContentModel {
393        self.content_model
394    }
395
396    fn content_mut(&mut self) -> Option<&mut Content<'src>> {
397        Some(&mut self.content)
398    }
399
400    fn rendered_content(&self) -> Option<&str> {
401        Some(self.content.rendered())
402    }
403
404    fn raw_context(&self) -> CowStr<'src> {
405        self.context.clone()
406    }
407
408    fn title_source(&'src self) -> Option<Span<'src>> {
409        self.title_source
410    }
411
412    fn title(&self) -> Option<&str> {
413        self.title.as_ref().map(Content::rendered_str)
414    }
415
416    fn caption(&self) -> Option<&str> {
417        self.caption.as_deref()
418    }
419
420    fn number(&self) -> Option<usize> {
421        self.number
422    }
423
424    fn anchor(&'src self) -> Option<Span<'src>> {
425        self.anchor
426    }
427
428    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
429        self.anchor_reftext
430    }
431
432    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
433        self.attrlist.as_ref()
434    }
435
436    fn substitution_group(&'src self) -> SubstitutionGroup {
437        self.substitution_group.clone()
438    }
439}
440
441impl<'src> HasSpan<'src> for RawDelimitedBlock<'src> {
442    fn span(&self) -> Span<'src> {
443        self.source
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    #![allow(clippy::unwrap_used)]
450
451    mod is_valid_delimiter {
452        use crate::blocks::RawDelimitedBlock;
453
454        #[test]
455        fn comment() {
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
467                "///"
468            )));
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                "//////////x"
477            )));
478        }
479
480        #[test]
481        fn example() {
482            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
483                "===="
484            )));
485            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
486                "====="
487            )));
488            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
489                "==="
490            )));
491        }
492
493        #[test]
494        fn listing() {
495            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
496                "----"
497            )));
498            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
499                "-----"
500            )));
501            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
502                "---------"
503            )));
504
505            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
506                "---"
507            )));
508            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
509                "--/-"
510            )));
511            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
512                "----/"
513            )));
514            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
515                "----------x"
516            )));
517        }
518
519        #[test]
520        fn fenced() {
521            // The fenced code block delimiter is exactly three backticks ...
522            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
523                "```"
524            )));
525
526            // ... optionally followed by a language on the opening fence.
527            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
528                "```java"
529            )));
530            assert!(RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
531                "```ruby"
532            )));
533
534            // A run of four or more backticks is not a fence (the backtick run
535            // has a fixed length, unlike the four-character delimiters).
536            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
537                "````"
538            )));
539            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
540                "``"
541            )));
542
543            // Backticks followed only by whitespace declare no language, so the
544            // line is not a language-aware fence.
545            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
546                "```\t"
547            )));
548            assert!(super::super::fenced_code_language(&crate::Span::new("```  ")).is_none());
549
550            // A language token is extracted from the info string, skipping any
551            // leading whitespace and ignoring anything after the first token.
552            assert_eq!(
553                super::super::fenced_code_language(&crate::Span::new("``` ruby extra"))
554                    .map(|s| s.data()),
555                Some("ruby")
556            );
557        }
558
559        #[test]
560        fn literal() {
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
572                "..."
573            )));
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                "..........x"
582            )));
583        }
584
585        #[test]
586        fn sidebar() {
587            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
588                "****"
589            )));
590            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
591                "*****"
592            )));
593            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
594                "***"
595            )));
596        }
597
598        #[test]
599        fn table() {
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                "!==="
611            )));
612        }
613
614        #[test]
615        fn pass() {
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            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
627                "+++"
628            )));
629            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
630                "++/+"
631            )));
632            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
633                "++++/"
634            )));
635            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
636                "++++++++++x"
637            )));
638        }
639
640        #[test]
641        fn quote() {
642            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
643                "____"
644            )));
645            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
646                "_____"
647            )));
648            assert!(!RawDelimitedBlock::is_valid_delimiter(&crate::Span::new(
649                "___"
650            )));
651        }
652    }
653
654    mod parse {
655        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
656
657        #[test]
658        fn err_invalid_delimiter() {
659            let mut parser = Parser::default();
660            assert!(
661                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new(""), &mut parser)
662                    .is_none()
663            );
664
665            let mut parser = Parser::default();
666            assert!(
667                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("..."), &mut parser)
668                    .is_none()
669            );
670
671            let mut parser = Parser::default();
672            assert!(
673                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("++++x"), &mut parser)
674                    .is_none()
675            );
676
677            let mut parser = Parser::default();
678            assert!(
679                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("____x"), &mut parser)
680                    .is_none()
681            );
682
683            let mut parser = Parser::default();
684            assert!(
685                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("====x"), &mut parser)
686                    .is_none()
687            );
688
689            let mut parser = Parser::default();
690            assert!(
691                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("==\n=="), &mut parser)
692                    .is_none()
693            );
694
695            let mut parser = Parser::default();
696            assert!(
697                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("===😀"), &mut parser)
698                    .is_none()
699            );
700        }
701
702        #[test]
703        fn err_unterminated() {
704            let mut parser = Parser::default();
705
706            let maw = crate::blocks::RawDelimitedBlock::parse(
707                &BlockMetadata::new("....\nblah blah blah"),
708                &mut parser,
709            )
710            .unwrap();
711
712            assert_eq!(
713                maw.warnings,
714                vec![Warning {
715                    source: Span {
716                        data: "....",
717                        line: 1,
718                        col: 1,
719                        offset: 0,
720                    },
721                    warning: WarningType::UnterminatedDelimitedBlock,
722                }]
723            );
724        }
725    }
726
727    mod comment {
728        use crate::{
729            blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
730            tests::prelude::*,
731        };
732
733        #[test]
734        fn empty() {
735            let mut parser = Parser::default();
736            let maw = crate::blocks::RawDelimitedBlock::parse(
737                &BlockMetadata::new("////\n////"),
738                &mut parser,
739            )
740            .unwrap();
741
742            let mi = maw.item.unwrap().clone();
743
744            assert_eq!(
745                mi.item,
746                RawDelimitedBlock {
747                    content: Content {
748                        original: Span {
749                            data: "",
750                            line: 2,
751                            col: 1,
752                            offset: 5,
753                        },
754                        rendered: "",
755                    },
756                    content_model: ContentModel::Raw,
757                    context: "comment",
758                    source: Span {
759                        data: "////\n////",
760                        line: 1,
761                        col: 1,
762                        offset: 0,
763                    },
764                    title_source: None,
765                    title: None,
766                    caption: None,
767                    number: None,
768                    anchor: None,
769                    anchor_reftext: None,
770                    attrlist: None,
771                    substitution_group: SubstitutionGroup::None,
772                }
773            );
774
775            assert_eq!(mi.item.content_model(), ContentModel::Raw);
776            assert_eq!(mi.item.rendered_content().unwrap(), "");
777            assert_eq!(mi.item.raw_context().as_ref(), "comment");
778            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
779            assert!(mi.item.declared_style().is_none());
780            assert!(mi.item.content().is_empty());
781            assert!(mi.item.id().is_none());
782            assert!(mi.item.roles().is_empty());
783            assert!(mi.item.options().is_empty());
784            assert!(mi.item.title_source().is_none());
785            assert!(mi.item.title().is_none());
786            assert!(mi.item.anchor().is_none());
787            assert!(mi.item.anchor_reftext().is_none());
788            assert!(mi.item.attrlist().is_none());
789            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
790        }
791
792        #[test]
793        fn multiple_lines() {
794            let mut parser = Parser::default();
795
796            let maw = crate::blocks::RawDelimitedBlock::parse(
797                &BlockMetadata::new("////\nline1  \nline2\n////"),
798                &mut parser,
799            )
800            .unwrap();
801
802            let mi = maw.item.unwrap().clone();
803
804            assert_eq!(
805                mi.item,
806                RawDelimitedBlock {
807                    content: Content {
808                        original: Span {
809                            data: "line1  \nline2",
810                            line: 2,
811                            col: 1,
812                            offset: 5,
813                        },
814                        rendered: "line1  \nline2",
815                    },
816                    content_model: ContentModel::Raw,
817                    context: "comment",
818                    source: Span {
819                        data: "////\nline1  \nline2\n////",
820                        line: 1,
821                        col: 1,
822                        offset: 0,
823                    },
824                    title_source: None,
825                    title: None,
826                    caption: None,
827                    number: None,
828                    anchor: None,
829                    anchor_reftext: None,
830                    attrlist: None,
831                    substitution_group: SubstitutionGroup::None,
832                }
833            );
834
835            assert_eq!(mi.item.content_model(), ContentModel::Raw);
836            assert_eq!(mi.item.rendered_content().unwrap(), "line1  \nline2");
837            assert_eq!(mi.item.raw_context().as_ref(), "comment");
838            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
839            assert!(mi.item.declared_style().is_none());
840            assert!(mi.item.id().is_none());
841            assert!(mi.item.roles().is_empty());
842            assert!(mi.item.options().is_empty());
843            assert!(mi.item.title_source().is_none());
844            assert!(mi.item.title().is_none());
845            assert!(mi.item.anchor().is_none());
846            assert!(mi.item.anchor_reftext().is_none());
847            assert!(mi.item.attrlist().is_none());
848            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
849
850            assert_eq!(
851                mi.item.content(),
852                Content {
853                    original: Span {
854                        data: "line1  \nline2",
855                        line: 2,
856                        col: 1,
857                        offset: 5,
858                    },
859                    rendered: "line1  \nline2",
860                }
861            );
862        }
863
864        #[test]
865        fn ignores_delimiter_prefix() {
866            let mut parser = Parser::default();
867
868            let maw = crate::blocks::RawDelimitedBlock::parse(
869                &BlockMetadata::new("////\nline1  \n/////\nline2\n////"),
870                &mut parser,
871            )
872            .unwrap();
873
874            let mi = maw.item.unwrap().clone();
875
876            assert_eq!(
877                mi.item,
878                RawDelimitedBlock {
879                    content: Content {
880                        original: Span {
881                            data: "line1  \n/////\nline2",
882                            line: 2,
883                            col: 1,
884                            offset: 5,
885                        },
886                        rendered: "line1  \n/////\nline2",
887                    },
888                    content_model: ContentModel::Raw,
889                    context: "comment",
890                    source: Span {
891                        data: "////\nline1  \n/////\nline2\n////",
892                        line: 1,
893                        col: 1,
894                        offset: 0,
895                    },
896                    title_source: None,
897                    title: None,
898                    caption: None,
899                    number: None,
900                    anchor: None,
901                    anchor_reftext: None,
902                    attrlist: None,
903                    substitution_group: SubstitutionGroup::None,
904                }
905            );
906
907            assert_eq!(mi.item.content_model(), ContentModel::Raw);
908            assert_eq!(mi.item.raw_context().as_ref(), "comment");
909            assert_eq!(mi.item.resolved_context().as_ref(), "comment");
910            assert!(mi.item.declared_style().is_none());
911            assert!(mi.item.id().is_none());
912            assert!(mi.item.roles().is_empty());
913            assert!(mi.item.options().is_empty());
914            assert!(mi.item.title_source().is_none());
915            assert!(mi.item.title().is_none());
916            assert!(mi.item.anchor().is_none());
917            assert!(mi.item.anchor_reftext().is_none());
918            assert!(mi.item.attrlist().is_none());
919            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::None);
920
921            assert_eq!(
922                mi.item.content(),
923                Content {
924                    original: Span {
925                        data: "line1  \n/////\nline2",
926                        line: 2,
927                        col: 1,
928                        offset: 5,
929                    },
930                    rendered: "line1  \n/////\nline2",
931                }
932            );
933        }
934
935        #[test]
936        fn no_panic_for_utf8_code_point_using_more_than_one_byte() {
937            let mut parser = Parser::default();
938            assert!(
939                crate::blocks::RawDelimitedBlock::parse(&BlockMetadata::new("///😀"), &mut parser)
940                    .is_none()
941            );
942        }
943    }
944
945    mod example {
946        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
947
948        #[test]
949        fn empty() {
950            let mut parser = Parser::default();
951            assert!(
952                crate::blocks::RawDelimitedBlock::parse(
953                    &BlockMetadata::new("====\n===="),
954                    &mut parser
955                )
956                .is_none()
957            );
958        }
959
960        #[test]
961        fn multiple_lines() {
962            let mut parser = Parser::default();
963            assert!(
964                crate::blocks::RawDelimitedBlock::parse(
965                    &BlockMetadata::new("====\nline1  \nline2\n===="),
966                    &mut parser
967                )
968                .is_none()
969            );
970        }
971    }
972
973    mod listing {
974        use crate::{
975            blocks::{ContentModel, metadata::BlockMetadata},
976            content::SubstitutionStep,
977            tests::prelude::*,
978        };
979
980        #[test]
981        fn empty() {
982            let mut parser = Parser::default();
983
984            let maw = crate::blocks::RawDelimitedBlock::parse(
985                &BlockMetadata::new("----\n----"),
986                &mut parser,
987            )
988            .unwrap();
989
990            let mi = maw.item.unwrap().clone();
991
992            assert_eq!(
993                mi.item,
994                RawDelimitedBlock {
995                    content: Content {
996                        original: Span {
997                            data: "",
998                            line: 2,
999                            col: 1,
1000                            offset: 5,
1001                        },
1002                        rendered: "",
1003                    },
1004                    content_model: ContentModel::Verbatim,
1005                    context: "listing",
1006                    source: Span {
1007                        data: "----\n----",
1008                        line: 1,
1009                        col: 1,
1010                        offset: 0,
1011                    },
1012                    title_source: None,
1013                    title: None,
1014                    caption: None,
1015                    number: None,
1016                    anchor: None,
1017                    anchor_reftext: None,
1018                    attrlist: None,
1019                    substitution_group: SubstitutionGroup::Verbatim,
1020                }
1021            );
1022
1023            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1024            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1025            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1026            assert!(mi.item.declared_style().is_none());
1027            assert!(mi.item.content().is_empty());
1028            assert!(mi.item.id().is_none());
1029            assert!(mi.item.roles().is_empty());
1030            assert!(mi.item.options().is_empty());
1031            assert!(mi.item.title_source().is_none());
1032            assert!(mi.item.title().is_none());
1033            assert!(mi.item.anchor().is_none());
1034            assert!(mi.item.anchor_reftext().is_none());
1035            assert!(mi.item.attrlist().is_none());
1036            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1037        }
1038
1039        #[test]
1040        fn multiple_lines() {
1041            let mut parser = Parser::default();
1042
1043            let maw = crate::blocks::RawDelimitedBlock::parse(
1044                &BlockMetadata::new("----\nline1  \nline2\n----"),
1045                &mut parser,
1046            )
1047            .unwrap();
1048
1049            let mi = maw.item.unwrap().clone();
1050
1051            assert_eq!(
1052                mi.item,
1053                RawDelimitedBlock {
1054                    content: Content {
1055                        original: Span {
1056                            data: "line1  \nline2",
1057                            line: 2,
1058                            col: 1,
1059                            offset: 5,
1060                        },
1061                        rendered: "line1  \nline2",
1062                    },
1063                    content_model: ContentModel::Verbatim,
1064                    context: "listing",
1065                    source: Span {
1066                        data: "----\nline1  \nline2\n----",
1067                        line: 1,
1068                        col: 1,
1069                        offset: 0,
1070                    },
1071                    title_source: None,
1072                    title: None,
1073                    caption: None,
1074                    number: None,
1075                    anchor: None,
1076                    anchor_reftext: None,
1077                    attrlist: None,
1078                    substitution_group: SubstitutionGroup::Verbatim,
1079                }
1080            );
1081
1082            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1083            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1084            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1085            assert!(mi.item.declared_style().is_none());
1086            assert!(mi.item.id().is_none());
1087            assert!(mi.item.roles().is_empty());
1088            assert!(mi.item.options().is_empty());
1089            assert!(mi.item.title_source().is_none());
1090            assert!(mi.item.title().is_none());
1091            assert!(mi.item.anchor().is_none());
1092            assert!(mi.item.anchor_reftext().is_none());
1093            assert!(mi.item.attrlist().is_none());
1094            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1095
1096            assert_eq!(
1097                mi.item.content(),
1098                Content {
1099                    original: Span {
1100                        data: "line1  \nline2",
1101                        line: 2,
1102                        col: 1,
1103                        offset: 5,
1104                    },
1105                    rendered: "line1  \nline2",
1106                }
1107            );
1108        }
1109
1110        #[test]
1111        fn overrides_sub_group_via_subs_attribute() {
1112            let mut parser = Parser::default();
1113
1114            let maw = crate::blocks::RawDelimitedBlock::parse(
1115                &BlockMetadata::new("[subs=quotes]\n----\nline1 < *line2*\n----"),
1116                &mut parser,
1117            )
1118            .unwrap();
1119
1120            let mi = maw.item.unwrap().clone();
1121
1122            assert_eq!(
1123                mi.item,
1124                RawDelimitedBlock {
1125                    content: Content {
1126                        original: Span {
1127                            data: "line1 < *line2*",
1128                            line: 3,
1129                            col: 1,
1130                            offset: 19,
1131                        },
1132                        rendered: "line1 < <strong>line2</strong>",
1133                    },
1134                    content_model: ContentModel::Verbatim,
1135                    context: "listing",
1136                    source: Span {
1137                        data: "[subs=quotes]\n----\nline1 < *line2*\n----",
1138                        line: 1,
1139                        col: 1,
1140                        offset: 0,
1141                    },
1142                    title_source: None,
1143                    title: None,
1144                    caption: None,
1145                    number: None,
1146                    anchor: None,
1147                    anchor_reftext: None,
1148                    attrlist: Some(Attrlist {
1149                        attributes: &[ElementAttribute {
1150                            name: Some("subs"),
1151                            value: "quotes",
1152                            shorthand_items: &[],
1153                        },],
1154                        anchor: None,
1155                        source: Span {
1156                            data: "subs=quotes",
1157                            line: 1,
1158                            col: 2,
1159                            offset: 1,
1160                        },
1161                    },),
1162                    substitution_group: SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes]),
1163                }
1164            );
1165
1166            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1167            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1168            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1169            assert!(mi.item.declared_style().is_none());
1170            assert!(mi.item.id().is_none());
1171            assert!(mi.item.roles().is_empty());
1172            assert!(mi.item.options().is_empty());
1173            assert!(mi.item.title_source().is_none());
1174            assert!(mi.item.title().is_none());
1175            assert!(mi.item.anchor().is_none());
1176            assert!(mi.item.anchor_reftext().is_none());
1177
1178            assert_eq!(
1179                mi.item.attrlist().unwrap(),
1180                Attrlist {
1181                    attributes: &[ElementAttribute {
1182                        name: Some("subs"),
1183                        value: "quotes",
1184                        shorthand_items: &[],
1185                    },],
1186                    anchor: None,
1187                    source: Span {
1188                        data: "subs=quotes",
1189                        line: 1,
1190                        col: 2,
1191                        offset: 1,
1192                    },
1193                }
1194            );
1195
1196            assert_eq!(
1197                mi.item.substitution_group(),
1198                SubstitutionGroup::Custom(vec![SubstitutionStep::Quotes])
1199            );
1200
1201            assert_eq!(
1202                mi.item.content(),
1203                Content {
1204                    original: Span {
1205                        data: "line1 < *line2*",
1206                        line: 3,
1207                        col: 1,
1208                        offset: 19,
1209                    },
1210                    rendered: "line1 < <strong>line2</strong>",
1211                }
1212            );
1213        }
1214
1215        #[test]
1216        fn ignores_delimiter_prefix() {
1217            let mut parser = Parser::default();
1218
1219            let maw = crate::blocks::RawDelimitedBlock::parse(
1220                &BlockMetadata::new("----\nline1  \n-----\nline2\n----"),
1221                &mut parser,
1222            )
1223            .unwrap();
1224
1225            let mi = maw.item.unwrap().clone();
1226
1227            assert_eq!(
1228                mi.item,
1229                RawDelimitedBlock {
1230                    content: Content {
1231                        original: Span {
1232                            data: "line1  \n-----\nline2",
1233                            line: 2,
1234                            col: 1,
1235                            offset: 5,
1236                        },
1237                        rendered: "line1  \n-----\nline2",
1238                    },
1239                    content_model: ContentModel::Verbatim,
1240                    context: "listing",
1241                    source: Span {
1242                        data: "----\nline1  \n-----\nline2\n----",
1243                        line: 1,
1244                        col: 1,
1245                        offset: 0,
1246                    },
1247                    title_source: None,
1248                    title: None,
1249                    caption: None,
1250                    number: None,
1251                    anchor: None,
1252                    anchor_reftext: None,
1253                    attrlist: None,
1254                    substitution_group: SubstitutionGroup::Verbatim,
1255                }
1256            );
1257
1258            assert_eq!(mi.item.content_model(), ContentModel::Verbatim);
1259            assert_eq!(mi.item.raw_context().as_ref(), "listing");
1260            assert_eq!(mi.item.resolved_context().as_ref(), "listing");
1261            assert!(mi.item.declared_style().is_none());
1262            assert!(mi.item.id().is_none());
1263            assert!(mi.item.roles().is_empty());
1264            assert!(mi.item.options().is_empty());
1265            assert!(mi.item.title_source().is_none());
1266            assert!(mi.item.title().is_none());
1267            assert!(mi.item.anchor().is_none());
1268            assert!(mi.item.anchor_reftext().is_none());
1269            assert!(mi.item.attrlist().is_none());
1270            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Verbatim);
1271
1272            assert_eq!(
1273                mi.item.content(),
1274                Content {
1275                    original: Span {
1276                        data: "line1  \n-----\nline2",
1277                        line: 2,
1278                        col: 1,
1279                        offset: 5,
1280                    },
1281                    rendered: "line1  \n-----\nline2",
1282                }
1283            );
1284
1285            assert_eq!(
1286                mi.item.content(),
1287                Content {
1288                    original: Span {
1289                        data: "line1  \n-----\nline2",
1290                        line: 2,
1291                        col: 1,
1292                        offset: 5,
1293                    },
1294                    rendered: "line1  \n-----\nline2",
1295                }
1296            );
1297        }
1298    }
1299
1300    mod sidebar {
1301        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1302
1303        #[test]
1304        fn empty() {
1305            let mut parser = Parser::default();
1306            assert!(
1307                crate::blocks::RawDelimitedBlock::parse(
1308                    &BlockMetadata::new("****\n****"),
1309                    &mut parser
1310                )
1311                .is_none()
1312            );
1313        }
1314
1315        #[test]
1316        fn multiple_lines() {
1317            let mut parser = Parser::default();
1318            assert!(
1319                crate::blocks::RawDelimitedBlock::parse(
1320                    &BlockMetadata::new("****\nline1  \nline2\n****"),
1321                    &mut parser
1322                )
1323                .is_none()
1324            );
1325        }
1326    }
1327
1328    mod table {
1329        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1330
1331        #[test]
1332        fn empty() {
1333            let mut parser = Parser::default();
1334            assert!(
1335                crate::blocks::RawDelimitedBlock::parse(
1336                    &BlockMetadata::new("|===\n|==="),
1337                    &mut parser
1338                )
1339                .is_none()
1340            );
1341
1342            let mut parser = Parser::default();
1343            assert!(
1344                crate::blocks::RawDelimitedBlock::parse(
1345                    &BlockMetadata::new(",===\n,==="),
1346                    &mut parser
1347                )
1348                .is_none()
1349            );
1350
1351            let mut parser = Parser::default();
1352            assert!(
1353                crate::blocks::RawDelimitedBlock::parse(
1354                    &BlockMetadata::new(":===\n:==="),
1355                    &mut parser
1356                )
1357                .is_none()
1358            );
1359
1360            let mut parser = Parser::default();
1361            assert!(
1362                crate::blocks::RawDelimitedBlock::parse(
1363                    &BlockMetadata::new("!===\n!==="),
1364                    &mut parser
1365                )
1366                .is_none()
1367            );
1368        }
1369
1370        #[test]
1371        fn multiple_lines() {
1372            let mut parser = Parser::default();
1373            assert!(
1374                crate::blocks::RawDelimitedBlock::parse(
1375                    &BlockMetadata::new("|===\nline1  \nline2\n|==="),
1376                    &mut parser
1377                )
1378                .is_none()
1379            );
1380
1381            let mut parser = Parser::default();
1382            assert!(
1383                crate::blocks::RawDelimitedBlock::parse(
1384                    &BlockMetadata::new(",===\nline1  \nline2\n,==="),
1385                    &mut parser
1386                )
1387                .is_none()
1388            );
1389
1390            let mut parser = Parser::default();
1391            assert!(
1392                crate::blocks::RawDelimitedBlock::parse(
1393                    &BlockMetadata::new(":===\nline1  \nline2\n:==="),
1394                    &mut parser
1395                )
1396                .is_none()
1397            );
1398
1399            let mut parser = Parser::default();
1400            assert!(
1401                crate::blocks::RawDelimitedBlock::parse(
1402                    &BlockMetadata::new("!===\nline1  \nline2\n!==="),
1403                    &mut parser
1404                )
1405                .is_none()
1406            );
1407        }
1408    }
1409
1410    mod pass {
1411        use crate::{
1412            blocks::{ContentModel, metadata::BlockMetadata},
1413            tests::prelude::*,
1414        };
1415
1416        #[test]
1417        fn empty() {
1418            let mut parser = Parser::default();
1419            let maw = crate::blocks::RawDelimitedBlock::parse(
1420                &BlockMetadata::new("++++\n++++"),
1421                &mut parser,
1422            )
1423            .unwrap();
1424
1425            let mi = maw.item.unwrap().clone();
1426
1427            assert_eq!(
1428                mi.item,
1429                RawDelimitedBlock {
1430                    content: Content {
1431                        original: Span {
1432                            data: "",
1433                            line: 2,
1434                            col: 1,
1435                            offset: 5,
1436                        },
1437                        rendered: "",
1438                    },
1439                    content_model: ContentModel::Raw,
1440                    context: "pass",
1441                    source: Span {
1442                        data: "++++\n++++",
1443                        line: 1,
1444                        col: 1,
1445                        offset: 0,
1446                    },
1447                    title_source: None,
1448                    title: None,
1449                    caption: None,
1450                    number: None,
1451                    anchor: None,
1452                    anchor_reftext: None,
1453                    attrlist: None,
1454                    substitution_group: SubstitutionGroup::Pass,
1455                }
1456            );
1457
1458            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1459            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1460            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1461            assert!(mi.item.declared_style().is_none());
1462            assert!(mi.item.content().is_empty());
1463            assert!(mi.item.id().is_none());
1464            assert!(mi.item.roles().is_empty());
1465            assert!(mi.item.options().is_empty());
1466            assert!(mi.item.title_source().is_none());
1467            assert!(mi.item.title().is_none());
1468            assert!(mi.item.anchor().is_none());
1469            assert!(mi.item.anchor_reftext().is_none());
1470            assert!(mi.item.attrlist().is_none());
1471            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1472        }
1473
1474        #[test]
1475        fn multiple_lines() {
1476            let mut parser = Parser::default();
1477
1478            let maw = crate::blocks::RawDelimitedBlock::parse(
1479                &BlockMetadata::new("++++\nline1  \nline2\n++++"),
1480                &mut parser,
1481            )
1482            .unwrap();
1483
1484            let mi = maw.item.unwrap().clone();
1485
1486            assert_eq!(
1487                mi.item,
1488                RawDelimitedBlock {
1489                    content: Content {
1490                        original: Span {
1491                            data: "line1  \nline2",
1492                            line: 2,
1493                            col: 1,
1494                            offset: 5,
1495                        },
1496                        rendered: "line1  \nline2",
1497                    },
1498                    content_model: ContentModel::Raw,
1499                    context: "pass",
1500                    source: Span {
1501                        data: "++++\nline1  \nline2\n++++",
1502                        line: 1,
1503                        col: 1,
1504                        offset: 0,
1505                    },
1506                    title_source: None,
1507                    title: None,
1508                    caption: None,
1509                    number: None,
1510                    anchor: None,
1511                    anchor_reftext: None,
1512                    attrlist: None,
1513                    substitution_group: SubstitutionGroup::Pass,
1514                }
1515            );
1516
1517            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1518            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1519            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1520            assert!(mi.item.declared_style().is_none());
1521            assert!(mi.item.id().is_none());
1522            assert!(mi.item.roles().is_empty());
1523            assert!(mi.item.options().is_empty());
1524            assert!(mi.item.title_source().is_none());
1525            assert!(mi.item.title().is_none());
1526            assert!(mi.item.anchor().is_none());
1527            assert!(mi.item.anchor_reftext().is_none());
1528            assert!(mi.item.attrlist().is_none());
1529            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1530
1531            assert_eq!(
1532                mi.item.content(),
1533                Content {
1534                    original: Span {
1535                        data: "line1  \nline2",
1536                        line: 2,
1537                        col: 1,
1538                        offset: 5,
1539                    },
1540                    rendered: "line1  \nline2",
1541                }
1542            );
1543        }
1544
1545        #[test]
1546        fn ignores_delimiter_prefix() {
1547            let mut parser = Parser::default();
1548
1549            let maw = crate::blocks::RawDelimitedBlock::parse(
1550                &BlockMetadata::new("++++\nline1  \n+++++\nline2\n++++"),
1551                &mut parser,
1552            )
1553            .unwrap();
1554
1555            let mi = maw.item.unwrap().clone();
1556
1557            assert_eq!(
1558                mi.item,
1559                RawDelimitedBlock {
1560                    content: Content {
1561                        original: Span {
1562                            data: "line1  \n+++++\nline2",
1563                            line: 2,
1564                            col: 1,
1565                            offset: 5,
1566                        },
1567                        rendered: "line1  \n+++++\nline2",
1568                    },
1569                    content_model: ContentModel::Raw,
1570                    context: "pass",
1571                    source: Span {
1572                        data: "++++\nline1  \n+++++\nline2\n++++",
1573                        line: 1,
1574                        col: 1,
1575                        offset: 0,
1576                    },
1577                    title_source: None,
1578                    title: None,
1579                    caption: None,
1580                    number: None,
1581                    anchor: None,
1582                    anchor_reftext: None,
1583                    attrlist: None,
1584                    substitution_group: SubstitutionGroup::Pass,
1585                }
1586            );
1587
1588            assert_eq!(mi.item.content_model(), ContentModel::Raw);
1589            assert_eq!(mi.item.raw_context().as_ref(), "pass");
1590            assert_eq!(mi.item.resolved_context().as_ref(), "pass");
1591            assert!(mi.item.declared_style().is_none());
1592            assert!(mi.item.id().is_none());
1593            assert!(mi.item.roles().is_empty());
1594            assert!(mi.item.options().is_empty());
1595            assert!(mi.item.title_source().is_none());
1596            assert!(mi.item.title().is_none());
1597            assert!(mi.item.anchor().is_none());
1598            assert!(mi.item.anchor_reftext().is_none());
1599            assert!(mi.item.attrlist().is_none());
1600            assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Pass);
1601
1602            assert_eq!(
1603                mi.item.content(),
1604                Content {
1605                    original: Span {
1606                        data: "line1  \n+++++\nline2",
1607                        line: 2,
1608                        col: 1,
1609                        offset: 5,
1610                    },
1611                    rendered: "line1  \n+++++\nline2",
1612                }
1613            );
1614        }
1615    }
1616
1617    mod stem {
1618        use crate::{blocks::ContentModel, tests::prelude::*};
1619
1620        /// A `[stem]` passthrough block becomes a `stem` block whose expression
1621        /// has only the special characters substitution applied. The notation's
1622        /// math delimiters are added by the converter at render time, so they
1623        /// do not appear in the parsed content.
1624        #[test]
1625        fn stem_style_block() {
1626            let doc = Parser::default().parse("[stem]\n++++\na < b\n++++");
1627            let block = doc.child_blocks().next().unwrap();
1628
1629            assert_eq!(block.content_model(), ContentModel::Raw);
1630            assert_eq!(block.raw_context().as_ref(), "stem");
1631            assert_eq!(block.resolved_context().as_ref(), "stem");
1632            assert_eq!(block.declared_style(), Some("stem"));
1633            assert_eq!(block.rendered_content(), Some("a &lt; b"));
1634            assert_eq!(block.substitution_group(), SubstitutionGroup::Stem);
1635            assert!(doc.warnings().next().is_none());
1636        }
1637
1638        #[test]
1639        fn asciimath_style_block() {
1640            let doc = Parser::default().parse("[asciimath]\n++++\nx^2\n++++");
1641            let block = doc.child_blocks().next().unwrap();
1642
1643            assert_eq!(block.raw_context().as_ref(), "stem");
1644            assert_eq!(block.declared_style(), Some("asciimath"));
1645            assert_eq!(block.rendered_content(), Some("x^2"));
1646        }
1647
1648        #[test]
1649        fn latexmath_style_block() {
1650            let doc = Parser::default().parse("[latexmath]\n++++\nC = \\alpha\n++++");
1651            let block = doc.child_blocks().next().unwrap();
1652
1653            assert_eq!(block.raw_context().as_ref(), "stem");
1654            assert_eq!(block.declared_style(), Some("latexmath"));
1655            assert_eq!(block.rendered_content(), Some(r"C = \alpha"));
1656        }
1657
1658        /// Without a STEM style, a `++++` block remains a `pass` block with no
1659        /// substitutions applied.
1660        #[test]
1661        fn unstyled_block_is_still_pass() {
1662            let doc = Parser::default().parse("++++\na < b\n++++");
1663            let block = doc.child_blocks().next().unwrap();
1664
1665            assert_eq!(block.raw_context().as_ref(), "pass");
1666            assert_eq!(block.rendered_content(), Some("a < b"));
1667            assert_eq!(block.substitution_group(), SubstitutionGroup::Pass);
1668        }
1669
1670        /// An explicit `subs` attribute still overrides a STEM block's default
1671        /// substitution group.
1672        #[test]
1673        fn subs_attribute_overrides_stem_default() {
1674            let doc = Parser::default().parse("[stem,subs=none]\n++++\na < b\n++++");
1675            let block = doc.child_blocks().next().unwrap();
1676
1677            assert_eq!(block.raw_context().as_ref(), "stem");
1678            assert_eq!(block.rendered_content(), Some("a < b"));
1679            assert_eq!(block.substitution_group(), SubstitutionGroup::None);
1680        }
1681    }
1682
1683    mod quote {
1684        use crate::{blocks::metadata::BlockMetadata, tests::prelude::*};
1685
1686        #[test]
1687        fn empty() {
1688            let mut parser = Parser::default();
1689            assert!(
1690                crate::blocks::RawDelimitedBlock::parse(
1691                    &BlockMetadata::new("____\n____"),
1692                    &mut parser
1693                )
1694                .is_none()
1695            );
1696        }
1697
1698        #[test]
1699        fn multiple_lines() {
1700            let mut parser = Parser::default();
1701            assert!(
1702                crate::blocks::RawDelimitedBlock::parse(
1703                    &BlockMetadata::new("____\nline1  \nline2\n____"),
1704                    &mut parser
1705                )
1706                .is_none()
1707            );
1708        }
1709    }
1710}